Merge branch 'master' into feat-previous-balance

This commit is contained in:
Moriz Wahl 2023-04-04 16:55:22 +02:00
commit c3ebc33ae5
47 changed files with 520 additions and 304 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
node: true, node: true,
}, },
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import'], plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import', 'n'],
extends: [ extends: [
'standard', 'standard',
'eslint:recommended', 'eslint:recommended',
@ -101,6 +101,45 @@ module.exports = {
}, },
], ],
'import/prefer-default-export': 'off', // TODO 'import/prefer-default-export': 'off', // TODO
// n
'n/handle-callback-err': 'error',
'n/no-callback-literal': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': 'error',
'n/no-extraneous-require': 'error',
'n/no-hide-core-modules': 'error',
'n/no-missing-import': 'off', // not compatible with typescript
'n/no-missing-require': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
'n/no-unpublished-require': 'error',
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
'n/callback-return': 'error',
'n/exports-style': 'error',
'n/file-extension-in-import': 'off',
'n/global-require': 'error',
'n/no-mixed-requires': 'error',
'n/no-process-env': 'error',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
'n/no-sync': 'error',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
}, },
overrides: [ overrides: [
// only for ts files // only for ts files

View File

@ -22,10 +22,12 @@ module.exports = {
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1', '@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1', '@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)': '@entity/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development' process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1' ? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1', : '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)': '@dbTools/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development' process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1' ? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1', : '<rootDir>/../database/build/src/$1',

View File

@ -65,6 +65,7 @@
"eslint-import-resolver-typescript": "^3.5.3", "eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1", "eslint-plugin-jest": "^27.2.1",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
@ -84,5 +85,8 @@
"ignore": [ "ignore": [
"**/*.test.ts" "**/*.test.ts"
] ]
},
"engines": {
"node": ">=14"
} }
} }

View File

@ -35,6 +35,7 @@ export enum RIGHTS {
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE', CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS', OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE', SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -34,6 +34,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.CREATE_CONTRIBUTION_MESSAGE, RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES, RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS, RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
]) ])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -1,4 +1,5 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
/* eslint-disable n/no-process-env */
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import dotenv from 'dotenv' import dotenv from 'dotenv'

View File

@ -4,7 +4,7 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType() @ArgsType()
export default class TransactionSendArgs { export default class TransactionSendArgs {
@Field(() => String) @Field(() => String)
email: string identifier: string
@Field(() => Decimal) @Field(() => Decimal)
amount: Decimal amount: Decimal

View File

@ -27,8 +27,6 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { findUserByEmail } from './UserResolver'
let mutate: any, query: any, con: any let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
@ -84,7 +82,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'wrong@email.com', identifier: 'wrong@email.com',
amount: 100, amount: 100,
memo: 'test', memo: 'test',
}, },
@ -112,22 +110,20 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'stephen@hawking.uk', identifier: 'stephen@hawking.uk',
amount: 100, amount: 100,
memo: 'test', memo: 'test',
}, },
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('The recipient account was deleted')], errors: [new GraphQLError('No user to given contact')],
}), }),
) )
}) })
it('logs the error thrown', async () => { it('logs the error thrown', () => {
// find peter to check the log expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk')
const user = await findUserByEmail('stephen@hawking.uk')
expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
}) })
}) })
@ -143,22 +139,23 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'garrick@ollivander.com', identifier: 'garrick@ollivander.com',
amount: 100, amount: 100,
memo: 'test', memo: 'test',
}, },
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('The recipient account is not activated')], errors: [new GraphQLError('No user with this credentials')],
}), }),
) )
}) })
it('logs the error thrown', async () => { it('logs the error thrown', () => {
// find peter to check the log expect(logger.error).toBeCalledWith(
const user = await findUserByEmail('garrick@ollivander.com') 'No user with this credentials',
expect(logger.error).toBeCalledWith('The recipient account is not activated', user) 'garrick@ollivander.com',
)
}) })
}) })
}) })
@ -178,7 +175,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'bob@baumeister.de', identifier: 'bob@baumeister.de',
amount: 100, amount: 100,
memo: 'test', memo: 'test',
}, },
@ -202,7 +199,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 100, amount: 100,
memo: 'test', memo: 'test',
}, },
@ -226,7 +223,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 100, amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t', memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
}, },
@ -250,7 +247,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 100, amount: 100,
memo: 'testing', memo: 'testing',
}, },
@ -300,7 +297,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: -50, amount: -50,
memo: 'testing negative', memo: 'testing negative',
}, },
@ -323,7 +320,7 @@ describe('send coins', () => {
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 50, amount: 50,
memo: 'unrepeatable memo', memo: 'unrepeatable memo',
}, },
@ -380,7 +377,7 @@ describe('send coins', () => {
mutate({ mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 10, amount: 10,
memo: 'first transaction', memo: 'first transaction',
}, },
@ -396,7 +393,7 @@ describe('send coins', () => {
mutate({ mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 20, amount: 20,
memo: 'second transaction', memo: 'second transaction',
}, },
@ -412,7 +409,7 @@ describe('send coins', () => {
mutate({ mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 30, amount: 30,
memo: 'third transaction', memo: 'third transaction',
}, },
@ -428,7 +425,7 @@ describe('send coins', () => {
mutate({ mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'peter@lustig.de', identifier: 'peter@lustig.de',
amount: 40, amount: 40,
memo: 'fourth transaction', memo: 'fourth transaction',
}, },

View File

@ -36,7 +36,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
import { BalanceResolver } from './BalanceResolver' import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver' import { findUserByIdentifier } from './util/findUserByIdentifier'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
export const executeTransaction = async ( export const executeTransaction = async (
@ -150,7 +150,6 @@ export const executeTransaction = async (
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.debug(`prepare Email for transaction received...`)
await sendTransactionReceivedEmail({ await sendTransactionReceivedEmail({
firstName: recipient.firstName, firstName: recipient.firstName,
lastName: recipient.lastName, lastName: recipient.lastName,
@ -302,10 +301,10 @@ export class TransactionResolver {
@Authorized([RIGHTS.SEND_COINS]) @Authorized([RIGHTS.SEND_COINS])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async sendCoins( async sendCoins(
@Args() { email, amount, memo }: TransactionSendArgs, @Args() { identifier, amount, memo }: TransactionSendArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`)
if (amount.lte(0)) { if (amount.lte(0)) {
throw new LogError('Amount to send must be positive', amount) throw new LogError('Amount to send must be positive', amount)
} }
@ -314,13 +313,9 @@ export class TransactionResolver {
const senderUser = getUser(context) const senderUser = getUser(context)
// validate recipient user // validate recipient user
const recipientUser = await findUserByEmail(email) const recipientUser = await findUserByIdentifier(identifier)
if (recipientUser.deletedAt) { if (!recipientUser) {
throw new LogError('The recipient account was deleted', recipientUser) throw new LogError('The recipient user was not found', recipientUser)
}
const emailContact = recipientUser.emailContact
if (!emailContact.emailChecked) {
throw new LogError('The recipient account is not activated', recipientUser)
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)

View File

@ -11,7 +11,7 @@ import { TransactionLink } from '@entity/TransactionLink'
import { User } from '@entity/User' import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact' import { UserContact } from '@entity/UserContact'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { validate as validateUUID, version as versionUUID } from 'uuid' import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
import { OptInType } from '@enum/OptInType' import { OptInType } from '@enum/OptInType'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
@ -46,7 +46,13 @@ import {
unDeleteUser, unDeleteUser,
sendActivationEmail, sendActivationEmail,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries' import {
verifyLogin,
queryOptIn,
searchAdminUsers,
searchUsers,
user as userQuery,
} from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
@ -2298,6 +2304,124 @@ describe('UserResolver', () => {
}) })
}) })
}) })
describe('user', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('unauthenticated', () => {
it('throws and logs "401 Unauthorized" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'identifier',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
expect(logger.error).toBeCalledWith('401 Unauthorized')
})
})
describe('authenticated', () => {
const uuid = uuidv4()
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
describe('identifier is no gradido ID and no email', () => {
it('throws and logs "Unknown identifier type" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'identifier',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Unknown identifier type')],
}),
)
expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier')
})
})
describe('identifier is not found', () => {
it('throws and logs "No user found to given identifier" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: uuid,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user found to given identifier')],
}),
)
expect(logger.error).toBeCalledWith('No user found to given identifier', uuid)
})
})
describe('identifier is found via email', () => {
it('returns user', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'bibi@bloxberg.de',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
errors: undefined,
}),
)
})
})
describe('identifier is found via gradidoID', () => {
it('returns user', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: user.gradidoID,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
errors: undefined,
}),
)
})
})
})
})
}) })
describe('printTimeDuration', () => { describe('printTimeDuration', () => {

View File

@ -72,6 +72,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint') const random = require('random-bigint')
@ -819,6 +820,12 @@ export class UserResolver {
return true return true
} }
@Authorized([RIGHTS.USER])
@Query(() => User)
async user(@Arg('identifier') identifier: string): Promise<User> {
return new User(await findUserByIdentifier(identifier))
}
} }
export async function findUserByEmail(email: string): Promise<DbUser> { export async function findUserByEmail(email: string): Promise<DbUser> {

View File

@ -152,7 +152,7 @@ describe('semaphore', () => {
}) })
const bibisTransaction = mutate({ const bibisTransaction = mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' }, variables: { identifier: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
}) })
await mutate({ await mutate({
mutation: login, mutation: login,
@ -168,7 +168,7 @@ describe('semaphore', () => {
}) })
const bobsTransaction = mutate({ const bobsTransaction = mutate({
mutation: sendCoins, mutation: sendCoins,
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' }, variables: { identifier: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
}) })
await mutate({ await mutate({
mutation: login, mutation: login,

View File

@ -0,0 +1,36 @@
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { validate, version } from 'uuid'
import LogError from '@/server/LogError'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
let user: DbUser | undefined
if (validate(identifier) && version(identifier) === 4) {
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
const userContact = await DbUserContact.findOne(
{
email: identifier,
emailChecked: true,
},
{ relations: ['user'] },
)
if (!userContact) {
throw new LogError('No user with this credentials', identifier)
}
if (!userContact.user) {
throw new LogError('No user to given contact', identifier)
}
user = userContact.user
user.emailContact = userContact
} else {
// last is alias when implemented
throw new LogError('Unknown identifier type', identifier)
}
return user
}

View File

@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// config
import CONFIG from './config' import CONFIG from './config'
import { startValidateCommunities } from './federation/validateCommunities' import { startValidateCommunities } from './federation/validateCommunities'
import createServer from './server/createServer' import createServer from './server/createServer'
@ -22,5 +20,5 @@ async function main() {
main().catch((e) => { main().catch((e) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e) console.error(e)
process.exit(1) throw e
}) })

View File

@ -28,6 +28,7 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
{ root, args, context, info }, { root, args, context, info },
next, next,
) => { ) => {
// eslint-disable-next-line n/callback-return
const result = await next() const result = await next()
let klickTipp = new KlickTipp({ status: 'Unsubscribed' }) let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
if (CONFIG.KLICKTIPP) { if (CONFIG.KLICKTIPP) {

View File

@ -75,8 +75,8 @@ export const sendActivationEmail = gql`
` `
export const sendCoins = gql` export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) { mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo) sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
} }
` `

View File

@ -340,3 +340,12 @@ export const listContributionMessages = gql`
} }
} }
` `
export const user = gql`
query ($identifier: String!) {
user(identifier: $identifier) {
firstName
lastName
}
}
`

View File

@ -61,6 +61,7 @@ ${JSON.stringify(requestContext.response.errors, null, 2)}`)
} }
const plugins = const plugins =
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin] process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins export default plugins

View File

@ -6,7 +6,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index' import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
@ -40,7 +39,6 @@ export const testEnvironment = async (testLogger: any = logger, testI18n: any =
const testClient = createTestClient(server.apollo) const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate const mutate = testClient.mutate
const query = testClient.query const query = testClient.query
await initialize()
return { mutate, query, con } return { mutate, query, con }
} }

View File

@ -1998,6 +1998,13 @@ buffer@^6.0.3:
base64-js "^1.3.1" base64-js "^1.3.1"
ieee754 "^1.2.1" ieee754 "^1.2.1"
builtins@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9"
integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==
dependencies:
semver "^7.0.0"
busboy@^0.3.1: busboy@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
@ -2937,6 +2944,14 @@ eslint-plugin-es@^3.0.0:
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
regexpp "^3.0.0" regexpp "^3.0.0"
eslint-plugin-es@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9"
integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==
dependencies:
eslint-utils "^2.0.0"
regexpp "^3.0.0"
eslint-plugin-import@^2.27.5: eslint-plugin-import@^2.27.5:
version "2.27.5" version "2.27.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65"
@ -2965,6 +2980,20 @@ eslint-plugin-jest@^27.2.1:
dependencies: dependencies:
"@typescript-eslint/utils" "^5.10.0" "@typescript-eslint/utils" "^5.10.0"
eslint-plugin-n@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz#f7e77f24abb92a550115cf11e29695da122c398c"
integrity sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==
dependencies:
builtins "^5.0.1"
eslint-plugin-es "^4.1.0"
eslint-utils "^3.0.0"
ignore "^5.1.1"
is-core-module "^2.11.0"
minimatch "^3.1.2"
resolve "^1.22.1"
semver "^7.3.8"
eslint-plugin-node@^11.1.0: eslint-plugin-node@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
@ -6300,7 +6329,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.7: semver@^7.0.0, semver@^7.3.7, semver@^7.3.8:
version "7.3.8" version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==

View File

@ -4,5 +4,3 @@ DB_USER=root
DB_PASSWORD= DB_PASSWORD=
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
MIGRATIONS_TABLE=migrations MIGRATIONS_TABLE=migrations
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}

View File

@ -6,5 +6,3 @@ DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
MIGRATIONS_TABLE=migrations MIGRATIONS_TABLE=migrations
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}

View File

@ -1,15 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const CONFIG = require('./src/config')
module.export = {
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
seeds: ['src/seeds/**/*{.ts,.js}'],
factories: ['src/factories/**/*{.ts,.js}'],
}

View File

@ -1,34 +0,0 @@
import CONFIG from './config'
import { createPool, PoolConfig } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import path from 'path'
const poolConfig: PoolConfig = {
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
}
// Pool?
const pool = createPool(poolConfig)
// Create & Initialize Migrations
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
silent: true,
dir: path.join(__dirname, '..', 'migrations'),
})
const initialize = async (): Promise<void> => {
await migration.initialize()
}
const resetDB = async (closePool = false): Promise<void> => {
await migration.reset() // use for resetting database
if (closePool) pool.end()
}
export { resetDB, pool, migration, initialize }

View File

@ -1,18 +1,29 @@
import 'reflect-metadata' import 'reflect-metadata'
import prepare from './prepare' import { createDatabase } from './prepare'
import connection from './typeorm/connection' import CONFIG from './config'
import { resetDB, pool, migration } from './helpers'
import { createPool } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import path from 'path'
const run = async (command: string) => { const run = async (command: string) => {
// Database actions not supported by our migration library // Database actions not supported by our migration library
await prepare() await createDatabase()
// Database connection for TypeORM
const con = await connection()
if (!con || !con.isConnected) {
throw new Error(`Couldn't open connection to database`)
}
// Initialize Migrations
const pool = createPool({
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
})
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
silent: true,
dir: path.join(__dirname, '..', 'migrations'),
})
await migration.initialize() await migration.initialize()
// Execute command // Execute command
@ -25,14 +36,13 @@ const run = async (command: string) => {
break break
case 'reset': case 'reset':
// TODO protect from production // TODO protect from production
await resetDB() // use for resetting database await migration.reset()
break break
default: default:
throw new Error(`Unsupported command ${command}`) throw new Error(`Unsupported command ${command}`)
} }
// Terminate connections gracefully // Terminate connections gracefully
await con.close()
pool.end() pool.end()
} }

View File

@ -1,12 +0,0 @@
import Decimal from 'decimal.js-light'
export interface TransactionContext {
typeId: number
userId: number
balance: Decimal
balanceDate: Date
amount: Decimal
memo: string
creationDate?: Date
sendReceiverUserId?: number
}

View File

@ -1,26 +0,0 @@
export interface UserContext {
pubKey?: Buffer
email?: string
firstName?: string
lastName?: string
deletedAt?: Date
password?: BigInt
privKey?: Buffer
emailHash?: Buffer
createdAt?: Date
emailChecked?: boolean
language?: string
publisherId?: number
passphrase?: string
}
export interface ServerUserContext {
username?: string
password?: string
email?: string
role?: string
activated?: number
lastLogin?: Date
created?: Date
modified?: Date
}

View File

@ -1,32 +0,0 @@
import Decimal from 'decimal.js-light'
export interface UserInterface {
// from user
email?: string
firstName?: string
lastName?: string
password?: BigInt
pubKey?: Buffer
privKey?: Buffer
emailHash?: Buffer
createdAt?: Date
emailChecked?: boolean
language?: string
deletedAt?: Date
publisherId?: number
passphrase?: string
// from server user
serverUserPassword?: string
role?: string
activated?: number
lastLogin?: Date
modified?: Date
// flag for admin
isAdmin?: boolean
// flag for balance (creation of 1000 GDD)
addBalance?: boolean
// balance
recordDate?: Date
creationDate?: Date
amount?: Decimal
}

View File

@ -1,15 +1,8 @@
/* PREPARE SCRIPT import { createConnection } from 'mysql2/promise'
*
* This file ensures operations our migration library
* can not take care of are done.
* This applies to all Database Operations like
* creating, deleting, renaming Databases.
*/
import { createConnection, RowDataPacket } from 'mysql2/promise'
import CONFIG from './config' import CONFIG from './config'
export default async (): Promise<void> => { export const createDatabase = async (): Promise<void> => {
const con = await createConnection({ const con = await createConnection({
host: CONFIG.DB_HOST, host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT, port: CONFIG.DB_PORT,
@ -25,6 +18,8 @@ export default async (): Promise<void> => {
DEFAULT CHARACTER SET utf8mb4 DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;`) DEFAULT COLLATE utf8mb4_unicode_ci;`)
/* LEGACY CODE
import { RowDataPacket } from 'mysql2/promise'
// Check if old migration table is present, delete if needed // Check if old migration table is present, delete if needed
const [rows] = await con.query(`SHOW TABLES FROM \`${CONFIG.DB_DATABASE}\` LIKE 'migrations';`) const [rows] = await con.query(`SHOW TABLES FROM \`${CONFIG.DB_DATABASE}\` LIKE 'migrations';`)
if ((<RowDataPacket>rows).length > 0) { if ((<RowDataPacket>rows).length > 0) {
@ -37,6 +32,7 @@ export default async (): Promise<void> => {
console.log('Found and dropped old migrations table') console.log('Found and dropped old migrations table')
} }
} }
*/
await con.end() await con.end()
} }

View File

@ -1,27 +0,0 @@
import { createConnection, Connection } from 'typeorm'
import CONFIG from '../config'
import { entities } from '../../entity/index'
const connection = async (): Promise<Connection | null> => {
let con = null
try {
con = await createConnection({
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
})
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
}
return con
}
export default connection

View File

@ -4,7 +4,6 @@
import CONFIG from '@/config' import CONFIG from '@/config'
import connection from '@/typeorm/connection' import connection from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion' import { checkDBVersion } from '@/typeorm/DBVersion'
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index' import { entities } from '@entity/index'
import { logger } from './testSetup' import { logger } from './testSetup'
@ -42,7 +41,6 @@ export const testEnvironment = async () => {
logger.fatal('Fatal: Database Version incorrect') logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect')
} }
await initialize()
return { con } return { con }
} }

View File

@ -5,9 +5,7 @@
<b-row class="mt-5"> <b-row class="mt-5">
<b-col cols="2"></b-col> <b-col cols="2"></b-col>
<b-col> <b-col>
<div class="h4"> <div class="h4">{{ userName ? userName : identifier }}</div>
{{ email }}
</div>
<div class="mt-3 h5">{{ $t('form.memo') }}</div> <div class="mt-3 h5">{{ $t('form.memo') }}</div>
<div>{{ memo }}</div> <div>{{ memo }}</div>
</b-col> </b-col>
@ -64,9 +62,10 @@ export default {
name: 'TransactionConfirmationSend', name: 'TransactionConfirmationSend',
props: { props: {
balance: { type: Number, required: true }, balance: { type: Number, required: true },
email: { type: String, required: false, default: '' }, identifier: { type: String, required: false, default: '' },
amount: { type: Number, required: true }, amount: { type: Number, required: true },
memo: { type: String, required: true }, memo: { type: String, required: true },
userName: { type: String, default: '' },
}, },
data() { data() {
return { return {

View File

@ -2,9 +2,17 @@ import { mount } from '@vue/test-utils'
import TransactionForm from './TransactionForm' import TransactionForm from './TransactionForm'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { SEND_TYPES } from '@/pages/Send' import { SEND_TYPES } from '@/pages/Send'
import DashboardLayout from '@/layouts/DashboardLayout' import { createMockClient } from 'mock-apollo-client'
import VueApollo from 'vue-apollo'
import { user as userQuery } from '@/graphql/queries'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue const localVue = global.localVue
localVue.use(VueApollo)
describe('TransactionForm', () => { describe('TransactionForm', () => {
let wrapper let wrapper
@ -22,6 +30,7 @@ describe('TransactionForm', () => {
}, },
$route: { $route: {
params: {}, params: {},
query: {},
}, },
} }
@ -34,10 +43,24 @@ describe('TransactionForm', () => {
localVue, localVue,
mocks, mocks,
propsData, propsData,
provide: DashboardLayout.provide, apolloProvider,
}) })
} }
const userQueryMock = jest.fn()
mockClient.setRequestHandler(
userQuery,
userQueryMock.mockRejectedValueOnce({ message: 'Query user name fails!' }).mockResolvedValue({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
}),
)
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
@ -139,7 +162,7 @@ describe('TransactionForm', () => {
.setValue(' valid@email.com ') .setValue(' valid@email.com ')
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur') await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur')
await flushPromises() await flushPromises()
expect(wrapper.vm.form.email).toBe('valid@email.com') expect(wrapper.vm.form.identifier).toBe('valid@email.com')
}) })
}) })
@ -290,12 +313,12 @@ Die ganze Welt bezwingen.“`)
.find('textarea') .find('textarea')
.setValue('Long enough') .setValue('Long enough')
await flushPromises() await flushPromises()
expect(wrapper.vm.form.email).toBe('someone@watches.tv') expect(wrapper.vm.form.identifier).toBe('someone@watches.tv')
expect(wrapper.vm.form.amount).toBe('87.23') expect(wrapper.vm.form.amount).toBe('87.23')
expect(wrapper.vm.form.memo).toBe('Long enough') expect(wrapper.vm.form.memo).toBe('Long enough')
await wrapper.find('button[type="reset"]').trigger('click') await wrapper.find('button[type="reset"]').trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.vm.form.email).toBe('') expect(wrapper.vm.form.identifier).toBe('')
expect(wrapper.vm.form.amount).toBe('') expect(wrapper.vm.form.amount).toBe('')
expect(wrapper.vm.form.memo).toBe('') expect(wrapper.vm.form.memo).toBe('')
}) })
@ -321,10 +344,11 @@ Die ganze Welt bezwingen.“`)
expect(wrapper.emitted('set-transaction')).toEqual([ expect(wrapper.emitted('set-transaction')).toEqual([
[ [
{ {
email: 'someone@watches.tv', identifier: 'someone@watches.tv',
amount: 87.23, amount: 87.23,
memo: 'Long enough', memo: 'Long enough',
selected: 'send', selected: 'send',
userName: '',
}, },
], ],
]) ])
@ -346,5 +370,26 @@ Die ganze Welt bezwingen.“`)
}) })
}) })
}) })
describe('with gradido ID', () => {
beforeEach(async () => {
jest.clearAllMocks()
mocks.$route.query.gradidoID = 'gradido-ID'
wrapper = Wrapper()
await wrapper.vm.$nextTick()
})
describe('query for username with success', () => {
it('has no email input field', () => {
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false)
})
it('queries the username', () => {
expect(userQueryMock).toBeCalledWith({
identifier: 'gradido-ID',
})
})
})
})
}) })
}) })

View File

@ -50,16 +50,24 @@
<b-col> <b-col>
<b-row> <b-row>
<b-col cols="12"> <b-col cols="12">
<div v-if="radioSelected === sendTypes.send"> <div v-if="radioSelected === sendTypes.send && !gradidoID">
<input-email <input-email
:name="$t('form.recipient')" :name="$t('form.recipient')"
:label="$t('form.recipient')" :label="$t('form.recipient')"
:placeholder="$t('form.email')" :placeholder="$t('form.email')"
v-model="form.email" v-model="form.identifier"
:disabled="isBalanceDisabled" :disabled="isBalanceDisabled"
@onValidation="onValidation" @onValidation="onValidation"
/> />
</div> </div>
<div v-else-if="gradidoID" class="mb-4">
<b-row>
<b-col>{{ $t('form.recipient') }}</b-col>
</b-row>
<b-row>
<b-col class="font-weight-bold">{{ userName }}</b-col>
</b-row>
</div>
</b-col> </b-col>
<b-col cols="12" lg="6"> <b-col cols="12" lg="6">
<input-amount <input-amount
@ -121,6 +129,7 @@ import { SEND_TYPES } from '@/pages/Send'
import InputEmail from '@/components/Inputs/InputEmail' import InputEmail from '@/components/Inputs/InputEmail'
import InputAmount from '@/components/Inputs/InputAmount' import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea' import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries'
export default { export default {
name: 'TransactionForm', name: 'TransactionForm',
@ -131,20 +140,20 @@ export default {
}, },
props: { props: {
balance: { type: Number, default: 0 }, balance: { type: Number, default: 0 },
email: { type: String, default: '' }, identifier: { type: String, default: '' },
amount: { type: Number, default: 0 }, amount: { type: Number, default: 0 },
memo: { type: String, default: '' }, memo: { type: String, default: '' },
selected: { type: String, default: 'send' }, selected: { type: String, default: 'send' },
}, },
inject: ['getTunneledEmail'],
data() { data() {
return { return {
form: { form: {
email: this.email, identifier: this.identifier,
amount: this.amount ? String(this.amount) : '', amount: this.amount ? String(this.amount) : '',
memo: this.memo, memo: this.memo,
}, },
radioSelected: this.selected, radioSelected: this.selected,
userName: '',
} }
}, },
methods: { methods: {
@ -152,33 +161,48 @@ export default {
this.$refs.formValidator.validate() this.$refs.formValidator.validate()
}, },
onSubmit() { onSubmit() {
if (this.gradidoID) this.form.identifier = this.gradidoID
this.$emit('set-transaction', { this.$emit('set-transaction', {
selected: this.radioSelected, selected: this.radioSelected,
email: this.form.email, identifier: this.form.identifier,
amount: Number(this.form.amount.replace(',', '.')), amount: Number(this.form.amount.replace(',', '.')),
memo: this.form.memo, memo: this.form.memo,
userName: this.userName,
}) })
}, },
onReset(event) { onReset(event) {
event.preventDefault() event.preventDefault()
this.form.email = '' this.form.identifier = ''
this.form.amount = '' this.form.amount = ''
this.form.memo = '' this.form.memo = ''
this.$refs.formValidator.validate() this.$refs.formValidator.validate()
}, if (this.$route.query && !this.$route.query === {}) this.$router.replace({ query: undefined })
setNewRecipientEmail() {
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
}, },
}, },
watch: { apollo: {
recipientEmail() { UserName: {
this.setNewRecipientEmail() query() {
return userQuery
},
fetchPolicy: 'network-only',
variables() {
return { identifier: this.gradidoID }
},
skip() {
return !this.gradidoID
},
update({ user }) {
this.userName = `${user.firstName} ${user.lastName}`
},
error({ message }) {
this.toastError(message)
},
}, },
}, },
computed: { computed: {
disabled() { disabled() {
if ( if (
this.form.email.length > 5 && this.form.identifier.length > 5 &&
parseInt(this.form.amount) <= parseInt(this.balance) && parseInt(this.form.amount) <= parseInt(this.balance) &&
this.form.memo.length > 5 && this.form.memo.length > 5 &&
this.form.memo.length <= 255 this.form.memo.length <= 255
@ -193,15 +217,12 @@ export default {
sendTypes() { sendTypes() {
return SEND_TYPES return SEND_TYPES
}, },
recipientEmail() { gradidoID() {
return this.getTunneledEmail() return this.$route.query && this.$route.query.gradidoID
}, },
}, },
created() {
this.setNewRecipientEmail()
},
mounted() { mounted() {
if (this.form.email !== '') this.$refs.formValidator.validate() if (this.form.identifier !== '') this.$refs.formValidator.validate()
}, },
} }
</script> </script>

View File

@ -31,15 +31,15 @@
class="pointer mb-3 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item" class="pointer mb-3 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
> >
<template #SEND> <template #SEND>
<transaction-send v-bind="transactions[index]" v-on="$listeners" /> <transaction-send v-bind="transactions[index]" />
</template> </template>
<template #RECEIVE> <template #RECEIVE>
<transaction-receive v-bind="transactions[index]" v-on="$listeners" /> <transaction-receive v-bind="transactions[index]" />
</template> </template>
<template #CREATION> <template #CREATION>
<transaction-creation v-bind="transactions[index]" v-on="$listeners" /> <transaction-creation v-bind="transactions[index]" />
</template> </template>
<template #LINK_SUMMARY> <template #LINK_SUMMARY>

View File

@ -32,11 +32,7 @@
<b-row> <b-row>
<b-col> <b-col>
<div class="font-weight-bold"> <div class="font-weight-bold">
<name <name :linkedUser="transaction.linkedUser" fontColor="text-dark" />
:linkedUser="transaction.linkedUser"
v-on="$listeners"
fontColor="text-dark"
/>
</div> </div>
<div class="d-flex mt-3"> <div class="d-flex mt-3">
<div class="small"> <div class="small">

View File

@ -3,9 +3,11 @@ import Name from './Name'
const localVue = global.localVue const localVue = global.localVue
const routerPushMock = jest.fn()
const mocks = { const mocks = {
$router: { $router: {
push: jest.fn(), push: routerPushMock,
history: { history: {
current: { current: {
fullPath: '/transactions', fullPath: '/transactions',
@ -47,7 +49,7 @@ describe('Name', () => {
describe('with linked user', () => { describe('with linked user', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.setProps({ await wrapper.setProps({
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de' }, linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', gradidoID: 'gradido-ID' },
}) })
}) })
@ -64,13 +66,17 @@ describe('Name', () => {
await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click') await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click')
}) })
it('emits set tunneled email', () => { it('pushes router to send', () => {
expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']]) expect(routerPushMock).toBeCalledWith({
path: '/send',
})
}) })
it('pushes the route with query for email', () => { it('pushes query for gradidoID', () => {
expect(mocks.$router.push).toBeCalledWith({ expect(routerPushMock).toBeCalledWith({
path: '/send', query: {
gradidoID: 'gradido-ID',
},
}) })
}) })
}) })

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="name"> <div class="name">
<div class="gdd-transaction-list-item-name"> <div class="gdd-transaction-list-item-name">
<div v-if="linkedUser && linkedUser.email"> <div v-if="linkedUser && linkedUser.gradidoID">
<b-link @click.stop="tunnelEmail" :class="fontColor"> <b-link @click.stop="tunnelEmail" :class="fontColor">
{{ itemText }} {{ itemText }}
</b-link> </b-link>
@ -35,8 +35,8 @@ export default {
}, },
methods: { methods: {
tunnelEmail() { tunnelEmail() {
this.$emit('set-tunneled-email', this.linkedUser.email)
if (this.$router.history.current.fullPath !== '/send') this.$router.push({ path: '/send' }) if (this.$router.history.current.fullPath !== '/send') this.$router.push({ path: '/send' })
this.$router.push({ query: { gradidoID: this.linkedUser.gradidoID } })
}, },
}, },
computed: { computed: {

View File

@ -14,7 +14,6 @@
<div> <div>
<name <name
class="font-weight-bold" class="font-weight-bold"
v-on="$listeners"
:amount="amount" :amount="amount"
:linkedUser="linkedUser" :linkedUser="linkedUser"
:linkId="linkId" :linkId="linkId"

View File

@ -13,7 +13,6 @@
<div> <div>
<name <name
class="font-weight-bold" class="font-weight-bold"
v-on="$listeners"
:amount="amount" :amount="amount"
:linkedUser="linkedUser" :linkedUser="linkedUser"
:linkId="linkId" :linkId="linkId"

View File

@ -69,8 +69,8 @@ export const createUser = gql`
` `
export const sendCoins = gql` export const sendCoins = gql`
mutation($email: String!, $amount: Decimal!, $memo: String!) { mutation($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo) sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
} }
` `
@ -144,6 +144,7 @@ export const createContributionMessage = gql`
export const login = gql` export const login = gql`
mutation($email: String!, $password: String!, $publisherId: Int) { mutation($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
gradidoID
email email
firstName firstName
lastName lastName

View File

@ -39,6 +39,7 @@ export const transactionsQuery = gql`
linkedUser { linkedUser {
firstName firstName
lastName lastName
gradidoID
email email
} }
decay { decay {
@ -269,3 +270,12 @@ export const openCreations = gql`
} }
} }
` `
export const user = gql`
query($identifier: String!) {
user(identifier: $identifier) {
firstName
lastName
}
}
`

View File

@ -174,15 +174,6 @@ describe('DashboardLayout', () => {
}) })
}) })
describe('set tunneled email', () => {
it('updates tunneled email', async () => {
await wrapper
.findComponent({ ref: 'router-view' })
.vm.$emit('set-tunneled-email', 'bibi@bloxberg.de')
expect(wrapper.vm.tunneledEmail).toBe('bibi@bloxberg.de')
})
})
it('has a component Navbar', () => { it('has a component Navbar', () => {
expect(wrapper.findComponent({ name: 'Navbar' }).exists()).toBe(true) expect(wrapper.findComponent({ name: 'Navbar' }).exists()).toBe(true)
}) })

View File

@ -127,7 +127,6 @@
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
/> />
</template> </template>
<template #community> <template #community>
@ -149,7 +148,6 @@
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
:pending="pending" :pending="pending"
@update-transactions="updateTransactions" @update-transactions="updateTransactions"
@set-tunneled-email="setTunneledEmail"
></router-view> ></router-view>
</fade-transition> </fade-transition>
</div> </div>
@ -164,7 +162,6 @@
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
/> />
</template> </template>
<template #empty /> <template #empty />
@ -234,18 +231,12 @@ export default {
transactionLinkCount: 0, transactionLinkCount: 0,
pending: true, pending: true,
visible: false, visible: false,
tunneledEmail: null,
hamburger: true, hamburger: true,
darkMode: false, darkMode: false,
skeleton: true, skeleton: true,
totalUsers: null, totalUsers: null,
} }
}, },
provide() {
return {
getTunneledEmail: () => this.tunneledEmail,
}
},
created() { created() {
this.updateTransactions(0) this.updateTransactions(0)
this.getCommunityStatistics() this.getCommunityStatistics()
@ -319,9 +310,6 @@ export default {
setVisible(bool) { setVisible(bool) {
this.visible = bool this.visible = bool
}, },
setTunneledEmail(email) {
this.tunneledEmail = email
},
}, },
} }
</script> </script>

View File

@ -10,6 +10,7 @@ const apolloMutationMock = jest.fn()
apolloMutationMock.mockResolvedValue('success') apolloMutationMock.mockResolvedValue('success')
const navigatorClipboardMock = jest.fn() const navigatorClipboardMock = jest.fn()
const routerPushMock = jest.fn()
const localVue = global.localVue const localVue = global.localVue
@ -38,6 +39,9 @@ describe('Send', () => {
$route: { $route: {
query: {}, query: {},
}, },
$router: {
push: routerPushMock,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -85,8 +89,8 @@ describe('Send', () => {
it('shows the transaction formular again', () => { it('shows the transaction formular again', () => {
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBe(true) expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBe(true)
}) })
// TODO:SKIPED at this point, a check must be made in the components ?
it.skip('restores the previous data in the formular', () => { it('restores the previous data in the formular', () => {
expect(wrapper.find("input[type='email']").vm.$el.value).toBe('user@example.org') expect(wrapper.find("input[type='email']").vm.$el.value).toBe('user@example.org')
expect(wrapper.find("input[type='text']").vm.$el.value).toBe('23.45') expect(wrapper.find("input[type='text']").vm.$el.value).toBe('23.45')
expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!') expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!')
@ -107,10 +111,11 @@ describe('Send', () => {
expect.objectContaining({ expect.objectContaining({
mutation: sendCoins, mutation: sendCoins,
variables: { variables: {
email: 'user@example.org', identifier: 'user@example.org',
amount: 23.45, amount: 23.45,
memo: 'Make the best of it!', memo: 'Make the best of it!',
selected: SEND_TYPES.send, selected: SEND_TYPES.send,
userName: '',
}, },
}), }),
) )
@ -162,6 +167,67 @@ describe('Send', () => {
}) })
}) })
describe('with gradidoID query', () => {
beforeEach(() => {
mocks.$route.query.gradidoID = 'gradido-ID'
wrapper = Wrapper()
})
it('has no email input field', () => {
expect(
wrapper.findComponent({ name: 'TransactionForm' }).find('input[type="email"]').exists(),
).toBe(false)
})
describe('submit form', () => {
beforeEach(async () => {
jest.clearAllMocks()
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
await transactionForm.find('input[type="text"]').setValue('34.56')
await transactionForm.find('textarea').setValue('Make the best of it!')
await transactionForm.find('form').trigger('submit')
await flushPromises()
})
it('steps forward in the dialog', () => {
expect(wrapper.findComponent({ name: 'TransactionConfirmationSend' }).exists()).toBe(true)
})
describe('confirm transaction', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper
.findComponent({ name: 'TransactionConfirmationSend' })
.find('button.btn-gradido')
.trigger('click')
})
it('calls the API', async () => {
expect(apolloMutationMock).toBeCalledWith(
expect.objectContaining({
mutation: sendCoins,
variables: {
identifier: 'gradido-ID',
amount: 34.56,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
userName: '',
},
}),
)
})
it('resets the gradido ID query in route', () => {
expect(routerPushMock).toBeCalledWith({
query: {
gradidoID: undefined,
},
})
})
})
})
})
describe('transaction form link', () => { describe('transaction form link', () => {
const now = new Date().toISOString() const now = new Date().toISOString()
beforeEach(async () => { beforeEach(async () => {

View File

@ -11,9 +11,7 @@
<template #transactionConfirmationSend> <template #transactionConfirmationSend>
<transaction-confirmation-send <transaction-confirmation-send
:balance="balance" :balance="balance"
:email="transactionData.email" v-bind="transactionData"
:amount="transactionData.amount"
:memo="transactionData.memo"
@send-transaction="sendTransaction" @send-transaction="sendTransaction"
@on-back="onBack" @on-back="onBack"
></transaction-confirmation-send> ></transaction-confirmation-send>
@ -21,7 +19,7 @@
<template #transactionConfirmationLink> <template #transactionConfirmationLink>
<transaction-confirmation-link <transaction-confirmation-link
:balance="balance" :balance="balance"
:email="transactionData.email" :email="transactionData.identifier"
:amount="transactionData.amount" :amount="transactionData.amount"
:memo="transactionData.memo" :memo="transactionData.memo"
:loading="loading" :loading="loading"
@ -62,7 +60,7 @@ import TransactionResultLink from '@/components/GddSend/TransactionResultLink'
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js' import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
const EMPTY_TRANSACTION_DATA = { const EMPTY_TRANSACTION_DATA = {
email: '', identifier: '',
amount: 0, amount: 0,
memo: '', memo: '',
} }
@ -168,6 +166,7 @@ export default {
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`) throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
} }
this.loading = false this.loading = false
this.$router.push({ query: { gradidoID: undefined } })
}, },
onBack() { onBack() {
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm this.currentTransactionStep = TRANSACTION_STEPS.transactionForm

View File

@ -17,7 +17,6 @@
:showPagination="true" :showPagination="true"
:pageSize="pageSize" :pageSize="pageSize"
@update-transactions="updateTransactions" @update-transactions="updateTransactions"
v-on="$listeners"
/> />
</div> </div>
</div> </div>