diff --git a/backend/jest.config.js b/backend/jest.config.js index 3b251916a..81ebbec55 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 86, + lines: 89, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 24fa4e48c..96d434a29 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -19,6 +19,7 @@ import { createContribution, login, sendCoins, + updateUserInfos, } from '@/seeds/graphql/mutations' import { transactionsQuery } from '@/seeds/graphql/queries' import { bobBaumeister } from '@/seeds/users/bob-baumeister' @@ -52,10 +53,13 @@ let bobData: any let peterData: any let user: User[] +let bob: User +let peter: User + describe('send coins', () => { beforeAll(async () => { - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bobBaumeister) + peter = await userFactory(testEnv, peterLustig) + bob = await userFactory(testEnv, bobBaumeister) await userFactory(testEnv, stephenHawking) await userFactory(testEnv, garrickOllivander) @@ -376,6 +380,114 @@ describe('send coins', () => { }) }) + describe('send coins via gradido ID', () => { + it('sends the coins', async () => { + await expect( + mutate({ + mutation: sendCoins, + variables: { + identifier: peter?.gradidoID, + amount: 10, + memo: 'send via gradido ID', + }, + }), + ).resolves.toMatchObject({ + data: { + sendCoins: true, + }, + errors: undefined, + }) + }) + }) + + describe('send coins via alias', () => { + beforeAll(async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bob', + }, + }) + await mutate({ + mutation: login, + variables: peterData, + }) + }) + + afterAll(async () => { + await mutate({ + mutation: login, + variables: bobData, + }) + }) + + it('sends the coins', async () => { + await expect( + mutate({ + mutation: sendCoins, + variables: { + identifier: 'bob', + amount: 6.66, + memo: 'send via alias', + }, + }), + ).resolves.toMatchObject({ + data: { + sendCoins: true, + }, + errors: undefined, + }) + }) + + describe("peter's transactions", () => { + it('has all expected transactions', async () => { + await expect(query({ query: transactionsQuery })).resolves.toMatchObject({ + data: { + transactionList: { + balance: expect.any(Object), + transactions: [ + expect.objectContaining({ + typeId: 'DECAY', + }), + expect.objectContaining({ + amount: expect.decimalEqual(-6.66), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'send via alias', + typeId: 'SEND', + }), + expect.objectContaining({ + amount: expect.decimalEqual(10), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'send via gradido ID', + typeId: 'RECEIVE', + }), + expect.objectContaining({ + amount: expect.decimalEqual(50), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'unrepeatable memo', + typeId: 'RECEIVE', + }), + ], + }, + }, + errors: undefined, + }) + }) + }) + }) + describe('more transactions to test semaphore', () => { it('sends the coins four times in a row', async () => { await expect( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 3c540b1f6..04f636b5a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -323,6 +323,7 @@ export class TransactionResolver { } // TODO this is subject to replay attacks + // --- WHY? const senderUser = getUser(context) // validate recipient user diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index f46d0a9bc..25787490f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2358,15 +2358,21 @@ describe('UserResolver', () => { mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibi', + }, + }) }) - describe('identifier is no gradido ID and no email', () => { + describe('identifier is no gradido ID, no email and no alias', () => { it('throws and logs "Unknown identifier type" error', async () => { await expect( query({ query: userQuery, variables: { - identifier: 'identifier', + identifier: 'identifier_is_no_valid_alias!', }, }), ).resolves.toEqual( @@ -2374,7 +2380,10 @@ describe('UserResolver', () => { errors: [new GraphQLError('Unknown identifier type')], }), ) - expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier') + expect(logger.error).toBeCalledWith( + 'Unknown identifier type', + 'identifier_is_no_valid_alias!', + ) }) }) @@ -2441,6 +2450,29 @@ describe('UserResolver', () => { ) }) }) + + describe('identifier is found via alias', () => { + it('returns user', async () => { + await expect( + query({ + query: userQuery, + variables: { + identifier: 'bibi', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + user: { + firstName: 'Bibi', + lastName: 'Bloxberg', + }, + }, + errors: undefined, + }), + ) + }) + }) }) }) diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index df932e544..bd9a25071 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -4,6 +4,8 @@ import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' +import { VALID_ALIAS_REGEX } from './validateAlias' + export const findUserByIdentifier = async (identifier: string): Promise => { let user: DbUser | undefined if (validate(identifier) && version(identifier) === 4) { @@ -27,8 +29,12 @@ export const findUserByIdentifier = async (identifier: string): Promise } user = userContact.user user.emailContact = userContact + } else if (VALID_ALIAS_REGEX.exec(identifier)) { + user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) + if (!user) { + throw new LogError('No user found to given identifier', identifier) + } } else { - // last is alias when implemented throw new LogError('Unknown identifier type', identifier) } diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index dcea7824c..721733be4 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -3,7 +3,10 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' -const reservedAlias = [ +// eslint-disable-next-line security/detect-unsafe-regex +export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ + +const RESERVED_ALIAS = [ 'admin', 'email', 'gast', @@ -24,10 +27,9 @@ const reservedAlias = [ export const validateAlias = async (alias: string): Promise => { if (alias.length < 3) throw new LogError('Given alias is too short', alias) if (alias.length > 20) throw new LogError('Given alias is too long', alias) - /* eslint-disable-next-line security/detect-unsafe-regex */ - if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) - throw new LogError('Invalid characters in alias', alias) - if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) + if (!alias.match(VALID_ALIAS_REGEX)) throw new LogError('Invalid characters in alias', alias) + if (RESERVED_ALIAS.includes(alias.toLowerCase())) + throw new LogError('Alias is not allowed', alias) const aliasInUse = await DbUser.find({ where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, })