diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index a0116a439..d312bc112 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -135,6 +135,7 @@ const permissions = shield( blockedUsers: isAuthenticated, notifications: isAuthenticated, profilePagePosts: or(onlyEnabledContent, isModerator), + Donations: isAuthenticated, }, Mutation: { '*': deny, @@ -177,6 +178,7 @@ const permissions = shield( VerifyEmailAddress: isAuthenticated, pinPost: isAdmin, unpinPost: isAdmin, + UpdateDonations: isAdmin, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/Donations.js b/backend/src/models/Donations.js new file mode 100644 index 000000000..45e06e1d4 --- /dev/null +++ b/backend/src/models/Donations.js @@ -0,0 +1,14 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + goal: { type: 'number' }, + progress: { type: 'number' }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 08362b69f..bd89ddc51 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -12,4 +12,5 @@ export default { Category: require('./Category.js'), Tag: require('./Tag.js'), Location: require('./Location.js'), + Donations: require('./Donations.js'), } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 95fa9ef61..b1bd36451 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -24,6 +24,7 @@ export default applyScalars( 'SocialMedia', 'NOTIFIED', 'REPORTED', + 'Donations', ], // add 'User' here as soon as possible }, @@ -44,6 +45,7 @@ export default applyScalars( 'EMOTED', 'NOTIFIED', 'REPORTED', + 'Donations', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js new file mode 100644 index 000000000..1c7aee8d4 --- /dev/null +++ b/backend/src/schema/resolvers/donations.js @@ -0,0 +1,31 @@ +export default { + Mutation: { + UpdateDonations: async (_parent, params, context, _resolveInfo) => { + const { driver } = context + const session = driver.session() + let donations + const writeTxResultPromise = session.writeTransaction(async txc => { + const updateDonationsTransactionResponse = await txc.run( + ` + MATCH (donations:Donations {id: $params.id}) + SET donations += $params + SET donations.updatedAt = toString(datetime()) + RETURN donations + `, + { params }, + ) + return updateDonationsTransactionResponse.records.map( + record => record.get('donations').properties, + ) + }) + try { + const txResult = await writeTxResultPromise + if (!txResult[0]) return null + donations = txResult[0] + } finally { + session.close() + } + return donations + }, + }, +} diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js new file mode 100644 index 000000000..a5264ee3a --- /dev/null +++ b/backend/src/schema/resolvers/donations.spec.js @@ -0,0 +1,172 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory from '../../seed/factories' +import { gql } from '../../jest/helpers' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' + +let mutate, query, authenticatedUser, variables +const factory = Factory() +const instance = getNeode() +const driver = getDriver() + +const updateDonationsMutation = gql` + mutation($id: ID!, $goal: Int, $progress: Int) { + UpdateDonations(id: $id, goal: $goal, progress: $progress) { + goal + progress + createdAt + updatedAt + } + } +` +const donationsQuery = gql` + query { + Donations { + goal + progress + } + } +` + +describe('donations', () => { + let currentUser, newlyCreatedDonations + beforeAll(async () => { + await factory.cleanDatabase() + authenticatedUser = undefined + const { server } = createServer({ + context: () => { + return { + driver, + neode: instance, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate + query = createTestClient(server).query + }) + + beforeEach(async () => { + variables = {} + newlyCreatedDonations = await factory.create('Donations', { id: 'total-donations' }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('query for donations', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = undefined + await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'normal-user', + role: 'user', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('returns the current Donations info', async () => { + await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ + data: { Donations: [{ goal: 15000, progress: 0 }] }, + }) + }) + }) + }) + }) + + describe('update donations', () => { + beforeEach(() => { + variables = { id: 'total-donations', goal: 20000, progress: 3000 } + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = undefined + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('authenticated', () => { + describe('as a normal user', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'normal-user', + role: 'user', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('as a moderator', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'moderator', + role: 'moderator', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('as an admin', () => { + beforeEach(async () => { + currentUser = await factory.create('User', { + id: 'admin', + role: 'admin', + }) + authenticatedUser = await currentUser.toJson() + }) + + it('updates Donations info', async () => { + await expect( + mutate({ mutation: updateDonationsMutation, variables }), + ).resolves.toMatchObject({ + data: { UpdateDonations: { goal: 20000, progress: 3000 } }, + errors: undefined, + }) + }) + + it('updates the updatedAt attribute', async () => { + newlyCreatedDonations = await newlyCreatedDonations.toJson() + const { + data: { UpdateDonations }, + } = await mutate({ mutation: updateDonationsMutation, variables }) + expect(newlyCreatedDonations.updatedAt).toBeTruthy() + expect(Date.parse(newlyCreatedDonations.updatedAt)).toEqual(expect.any(Number)) + expect(UpdateDonations.updatedAt).toBeTruthy() + expect(Date.parse(UpdateDonations.updatedAt)).toEqual(expect.any(Number)) + expect(newlyCreatedDonations.updatedAt).not.toEqual(UpdateDonations.updatedAt) + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/schema/types/type/Donations.gql new file mode 100644 index 000000000..88e5edd2d --- /dev/null +++ b/backend/src/schema/types/type/Donations.gql @@ -0,0 +1,14 @@ +type Donations { + goal: Int! + progress: Int! + createdAt: String + updatedAt: String +} + +type Query { + Donations: [Donations] +} + +type Mutation { + UpdateDonations(id: ID!, goal: Int, progress: Int): Donations +} \ No newline at end of file diff --git a/backend/src/seed/factories/donations.js b/backend/src/seed/factories/donations.js new file mode 100644 index 000000000..e22cdb6d7 --- /dev/null +++ b/backend/src/seed/factories/donations.js @@ -0,0 +1,18 @@ +import uuid from 'uuid/v4' + +export default function create() { + return { + factory: async ({ args, neodeInstance }) => { + const defaults = { + id: uuid(), + goal: 15000, + progress: 0, + } + args = { + ...defaults, + ...args, + } + return neodeInstance.create('Donations', args) + }, + } +} diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 2c96c8698..5054155fc 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -8,6 +8,7 @@ import createTag from './tags.js' import createSocialMedia from './socialMedia.js' import createLocation from './locations.js' import createEmailAddress from './emailAddresses.js' +import createDonations from './donations.js' import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js' const factories = { @@ -21,6 +22,7 @@ const factories = { Location: createLocation, EmailAddress: createEmailAddress, UnverifiedEmailAddress: createUnverifiedEmailAddresss, + Donations: createDonations, } export const cleanDatabase = async (options = {}) => {