diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 639326a64..d5e2cc7ce 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -26,6 +26,7 @@ export enum RIGHTS { LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', GDT_BALANCE = 'GDT_BALANCE', CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION', + DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION', LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS', LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS', UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 935bd94e5..9dcba0a4b 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -24,6 +24,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_TRANSACTION_LINKS, RIGHTS.GDT_BALANCE, RIGHTS.CREATE_CONTRIBUTION, + RIGHTS.DELETE_CONTRIBUTION, RIGHTS.LIST_CONTRIBUTIONS, RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 37ff45f25..b584624c2 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -4,7 +4,9 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { adminUpdateContribution, + confirmContribution, createContribution, + deleteContribution, updateContribution, } from '@/seeds/graphql/mutations' import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries' @@ -487,6 +489,11 @@ describe('ContributionResolver', () => { }) }) + afterAll(async () => { + await cleanDB() + resetToken() + }) + it('returns allCreation', async () => { await expect( query({ @@ -522,4 +529,129 @@ describe('ContributionResolver', () => { }) }) }) + + describe('deleteContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: deleteContribution, + variables: { + id: -1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + result = await mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('wrong contribution id', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: -1, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution not found for given id.')], + }), + ) + }) + }) + + describe('other user sends a deleteContribtuion', () => { + it('returns an error', async () => { + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Can not delete contribution of another user')], + }), + ) + }) + }) + + describe('User deletes own contribution', () => { + it('deletes successfully', async () => { + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toBeTruthy() + }) + }) + + describe('User deletes already confirmed contribution', () => { + it('throws an error', async () => { + await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: confirmContribution, + variables: { + id: result.data.createContribution.id, + }, + }) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await expect( + mutate({ + mutation: deleteContribution, + variables: { + id: result.data.createContribution.id, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('A confirmed contribution can not be deleted')], + }), + ) + }) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index f21c65068..ef4467a71 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -38,6 +38,27 @@ export class ContributionResolver { return new UnconfirmedContribution(contribution, user, creations) } + @Authorized([RIGHTS.DELETE_CONTRIBUTION]) + @Mutation(() => Boolean) + async deleteContribution( + @Arg('id', () => Int) id: number, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + const contribution = await dbContribution.findOne(id) + if (!contribution) { + throw new Error('Contribution not found for given id.') + } + if (contribution.userId !== user.id) { + throw new Error('Can not delete contribution of another user') + } + if (contribution.confirmedAt) { + throw new Error('A confirmed contribution can not be deleted') + } + const res = await contribution.softRemove() + return !!res + } + @Authorized([RIGHTS.LIST_CONTRIBUTIONS]) @Query(() => ContributionListResult) async listContributions( diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 4e7fa8a90..bf898bd7d 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -255,3 +255,9 @@ export const updateContribution = gql` } } ` + +export const deleteContribution = gql` + mutation ($id: Int!) { + deleteContribution(id: $id) + } +`