diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index dcd0e44d6..f8fa38616 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -38,4 +38,6 @@ export enum RIGHTS { CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST', LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN', CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', + LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', + DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', } diff --git a/backend/src/graphql/model/ContributionLinkList.ts b/backend/src/graphql/model/ContributionLinkList.ts new file mode 100644 index 000000000..412d0bf7b --- /dev/null +++ b/backend/src/graphql/model/ContributionLinkList.ts @@ -0,0 +1,11 @@ +import { ObjectType, Field } from 'type-graphql' +import { ContributionLink } from '@model/ContributionLink' + +@ObjectType() +export class ContributionLinkList { + @Field(() => [ContributionLink]) + links: ContributionLink[] + + @Field(() => Number) + count: number +} diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 6b0f295c7..32227f257 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -21,12 +21,14 @@ import { deletePendingCreation, confirmPendingCreation, createContributionLink, + deleteContributionLink, } from '@/seeds/graphql/mutations' import { getPendingCreations, login, searchUsers, listTransactionLinksAdmin, + listContributionLinks, } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { User } from '@entity/User' @@ -1610,7 +1612,7 @@ describe('AdminResolver', () => { describe('unauthenticated', () => { describe('createContributionLink', () => { - it.only('returns an error', async () => { + it('returns an error', async () => { await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], @@ -1618,6 +1620,28 @@ describe('AdminResolver', () => { ) }) }) + + describe('listContributionLinks', () => { + it('returns an error', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('deleteContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) }) describe('authenticated', () => { @@ -1636,7 +1660,7 @@ describe('AdminResolver', () => { }) describe('createContributionLink', () => { - it.only('returns an error', async () => { + it('returns an error', async () => { await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], @@ -1644,6 +1668,28 @@ describe('AdminResolver', () => { ) }) }) + + describe('listContributionLinks', () => { + it('returns an error', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('deleteContributionLink', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) }) describe('with admin rights', () => { @@ -1661,7 +1707,7 @@ describe('AdminResolver', () => { }) describe('createContributionLink', () => { - it.only('returns a contribution link object', async () => { + it('returns a contribution link object', async () => { await expect(mutate({ mutation: createContributionLink, variables })).resolves.toEqual( expect.objectContaining({ data: { @@ -1683,7 +1729,7 @@ describe('AdminResolver', () => { ) }) - it.only('has a contribution link stored in db', async () => { + it('has a contribution link stored in db', async () => { const cls = await DbContributionLink.find() expect(cls).toHaveLength(1) expect(cls[0]).toEqual( @@ -1708,6 +1754,84 @@ describe('AdminResolver', () => { ) }) }) + + describe('listContributionLinks', () => { + describe('one link in DB', () => { + it('returns the link and count 1', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + data: { + listContributionLinks: { + links: expect.arrayContaining([ + expect.objectContaining({ + amount: '200', + code: expect.stringMatching(/^CL-[0-9a-f]{24,24}$/), + link: expect.any(String), + createdAt: expect.any(String), + name: 'Dokumenta 2022', + memo: 'Danke für deine Teilnahme an der Dokumenta 2022', + validFrom: expect.any(String), + validTo: expect.any(String), + maxAmountPerMonth: '200', + cycle: 'once', + maxPerCycle: 1, + }), + ]), + count: 1, + }, + }, + }), + ) + }) + }) + }) + + describe('deleteContributionLink', () => { + describe('no valid id', () => { + it('returns an error', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: -1 } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Contribution Link not found to given id.')], + }), + ) + }) + }) + + describe('valid id', () => { + let linkId: number + beforeAll(async () => { + const links = await query({ query: listContributionLinks }) + linkId = links.data.listContributionLinks.links[0].id + }) + + it('returns a date string', async () => { + await expect( + mutate({ mutation: deleteContributionLink, variables: { id: linkId } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + deleteContributionLink: expect.any(String), + }, + }), + ) + }) + + it('does not list this contribution link anymore', async () => { + await expect(query({ query: listContributionLinks })).resolves.toEqual( + expect.objectContaining({ + data: { + listContributionLinks: { + links: [], + count: 0, + }, + }, + }), + ) + }) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index d7c843611..715d09d35 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -1,4 +1,5 @@ import { Context, getUser } from '@/server/context' +import { backendLogger as logger } from '@/server/logger' import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql' import { getCustomRepository, @@ -15,6 +16,7 @@ import { PendingCreation } from '@model/PendingCreation' import { CreatePendingCreations } from '@model/CreatePendingCreations' import { UpdatePendingCreation } from '@model/UpdatePendingCreation' import { ContributionLink } from '@model/ContributionLink' +import { ContributionLinkList } from '@model/ContributionLinkList' import { RIGHTS } from '@/auth/RIGHTS' import { UserRepository } from '@repository/User' import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs' @@ -491,9 +493,39 @@ export class AdminResolver { if (validTo) dbContributionLink.validTo = new Date(validTo) dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxPerCycle = maxPerCycle - dbContributionLink.save() + await dbContributionLink.save() return new ContributionLink(dbContributionLink) } + + @Authorized([RIGHTS.LIST_CONTRIBUTION_LINKS]) + @Query(() => ContributionLinkList) + async listContributionLinks( + @Args() + { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, + ): Promise { + const [links, count] = await DbContributionLink.findAndCount({ + order: { createdAt: order }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + return { + links: links.map((link: DbContributionLink) => new ContributionLink(link)), + count, + } + } + + @Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK]) + @Mutation(() => Date, { nullable: true }) + async deleteContributionLink(@Arg('id', () => Int) id: number): Promise { + const contributionLink = await DbContributionLink.findOne(id) + if (!contributionLink) { + logger.error(`Contribution Link not found to given id: ${id}`) + throw new Error('Contribution Link not found to given id.') + } + await contributionLink.softRemove() + const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true }) + return newContributionLink ? newContributionLink.deletedAt : null + } } interface CreationMap { diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 355b5aa79..7c334796e 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -173,3 +173,9 @@ export const createContributionLink = gql` } } ` + +export const deleteContributionLink = gql` + mutation ($id: Int!) { + deleteContributionLink(id: $id) + } +` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 03ee3b53e..9a0e00be3 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -217,3 +217,25 @@ export const listTransactionLinksAdmin = gql` } } ` + +export const listContributionLinks = gql` + query ($pageSize: Int = 25, $currentPage: Int = 1, $order: Order) { + listContributionLinks(pageSize: $pageSize, currentPage: $currentPage, order: $order) { + links { + id + amount + name + memo + code + link + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + count + } + } +`