From 9703e2d5e5d34a419ec176b80baff38d33deb185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 21 Mar 2019 23:53:29 +0100 Subject: [PATCH] Import changes from @Tirokk --- backend/src/graphql-schema.js | 4 +- .../src/middleware/permissionsMiddleware.js | 5 + backend/src/resolvers/rewards.js | 47 ++++ backend/src/resolvers/rewards.spec.js | 232 ++++++++++++++++++ backend/src/schema.graphql | 10 + 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 backend/src/resolvers/rewards.js create mode 100644 backend/src/resolvers/rewards.spec.js diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js index b9cdfdb37..57b2ffb6c 100644 --- a/backend/src/graphql-schema.js +++ b/backend/src/graphql-schema.js @@ -6,6 +6,7 @@ import statistics from './resolvers/statistics.js' import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' +import rewards from './resolvers/rewards.js' export const typeDefs = fs .readFileSync( @@ -21,7 +22,8 @@ export const resolvers = { Mutation: { ...userManagement.Mutation, ...reports.Mutation, + ...posts.Mutation, ...moderation.Mutation, - ...posts.Mutation + ...rewards.Mutation } } diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 62999c235..736ce20a9 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -56,6 +56,11 @@ const permissions = shield({ CreateBadge: isAdmin, UpdateBadge: isAdmin, DeleteBadge: isAdmin, + AddUserBadges: isAdmin, + // AddBadgeRewarded: isAdmin, + // RemoveBadgeRewarded: isAdmin, + reward: isAdmin, + unreward: isAdmin, // addFruitToBasket: isAuthenticated follow: isAuthenticated, unfollow: isAuthenticated, diff --git a/backend/src/resolvers/rewards.js b/backend/src/resolvers/rewards.js new file mode 100644 index 000000000..6bf6ddeea --- /dev/null +++ b/backend/src/resolvers/rewards.js @@ -0,0 +1,47 @@ +export default { + Mutation: { + reward: async (_object, params, context, _resolveInfo) => { + const { fromBadgeId, toUserId } = params + const session = context.driver.session() + + let sessionRes = await session.run( + `MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId}) + MERGE (badge)-[:REWARDED]->(rewardedUser) + RETURN rewardedUser {.id}`, + { + badgeId: fromBadgeId, + rewardedUserId: toUserId + } + ) + + const [rewardedUser] = sessionRes.records.map(record => { + return record.get('rewardedUser') + }) + + session.close() + + return rewardedUser.id + }, + + unreward: async (_object, params, context, _resolveInfo) => { + const { fromBadgeId, toUserId } = params + const session = context.driver.session() + + let sessionRes = await session.run( + `MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId}) + DELETE reward + RETURN rewardedUser {.id}`, + { + badgeId: fromBadgeId, + rewardedUserId: toUserId + } + ) + const [rewardedUser] = sessionRes.records.map(record => { + return record.get('rewardedUser') + }) + session.close() + + return rewardedUser.id + } + } +} diff --git a/backend/src/resolvers/rewards.spec.js b/backend/src/resolvers/rewards.spec.js new file mode 100644 index 000000000..567228eca --- /dev/null +++ b/backend/src/resolvers/rewards.spec.js @@ -0,0 +1,232 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +describe('rewards', () => { + beforeEach(async () => { + await factory.create('User', { + id: 'u1', + role: 'user', + email: 'user@example.org', + password: '1234' + }) + await factory.create('User', { + id: 'u2', + role: 'moderator', + email: 'moderator@example.org' + }) + await factory.create('User', { + id: 'u3', + role: 'admin', + email: 'admin@example.org' + }) + await factory.create('Badge', { + id: 'b6', + key: 'indiegogo_en_rhino', + type: 'crowdfunding', + status: 'permanent', + icon: '/img/badges/indiegogo_en_rhino.svg' + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('RewardBadge', () => { + const mutation = ` + mutation( + $from: ID! + $to: ID! + ) { + reward(fromBadgeId: $from, toUserId: $to) + } + ` + + describe('unauthenticated', () => { + const variables = { + from: 'b6', + to: 'u1' + } + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated admin', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('rewards a badge to user', async () => { + const variables = { + from: 'b6', + to: 'u1' + } + const expected = { + reward: 'u1' + } + await expect( + client.request(mutation, variables) + ).resolves.toEqual(expected) + }) + it('rewards a second different badge to same user', async () => { + await factory.create('Badge', { + id: 'b1', + key: 'indiegogo_en_racoon', + type: 'crowdfunding', + status: 'permanent', + icon: '/img/badges/indiegogo_en_racoon.svg' + }) + const variables = { + from: 'b1', + to: 'u1' + } + const expected = { + reward: 'u1' + } + await expect( + client.request(mutation, variables) + ).resolves.toEqual(expected) + }) + it('rewards the same badge as well to another user', async () => { + const variables1 = { + from: 'b6', + to: 'u1' + } + await client.request(mutation, variables1) + + const variables2 = { + from: 'b6', + to: 'u2' + } + const expected = { + reward: 'u2' + } + await expect( + client.request(mutation, variables2) + ).resolves.toEqual(expected) + }) + it('returns the original reward if a reward is attempted a second time', async () => { + const variables = { + from: 'b6', + to: 'u1' + } + await client.request(mutation, variables) + await client.request(mutation, variables) + + const query = `{ + User( id: "u1" ) { + badgesCount + } + } + ` + const expected = { User: [{ badgesCount: 1 }] } + + await expect( + client.request(query) + ).resolves.toEqual(expected) + }) + }) + + describe('authenticated moderator', () => { + const variables = { + from: 'b6', + to: 'u1' + } + let client + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('rewards bage to user', () => { + it('throws authorization error', async () => { + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + }) + }) + + describe('RemoveReward', () => { + beforeEach(async () => { + await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' }) + }) + const variables = { + from: 'b6', + to: 'u1' + } + const expected = { + unreward: 'u1' + } + + const mutation = ` + mutation( + $from: ID! + $to: ID! + ) { + unreward(fromBadgeId: $from, toUserId: $to) + } + ` + + describe('unauthenticated', () => { + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated admin', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('removes a badge from user', async () => { + await expect( + client.request(mutation, variables) + ).resolves.toEqual(expected) + }) + + it('fails to remove a not existing badge from user', async () => { + await client.request(mutation, variables) + + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Cannot read property \'id\' of undefined') + }) + }) + + describe('authenticated moderator', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('removes bage from user', () => { + it('throws authorization error', async () => { + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + }) + }) +}) diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index af7764f33..3b1d95a95 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -25,6 +25,8 @@ type Mutation { report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID + reward(fromBadgeId: ID!, toUserId: ID!): ID + unreward(fromBadgeId: ID!, toUserId: ID!): ID "Shout the given Type and ID" shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) @@ -269,6 +271,14 @@ enum FollowTypeEnum { Project } +type Reward { + id: ID! + user: User @relation(name: "REWARDED", direction: "IN") + rewarderId: ID + createdAt: String + badge: Badge @relation(name: "REWARDED", direction: "OUT") +} + type Organization { id: ID! createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")