mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2837 from gradido/feat-send-coins-via-gradido-ID
feat(frontend): send coins via gradido ID
This commit is contained in:
commit
acaf4fc599
@ -35,6 +35,7 @@ export enum RIGHTS {
|
||||
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
OPEN_CREATIONS = 'OPEN_CREATIONS',
|
||||
USER = 'USER',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
|
||||
@ -34,6 +34,7 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
RIGHTS.OPEN_CREATIONS,
|
||||
RIGHTS.USER,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { ArgsType, Field } from 'type-graphql'
|
||||
@ArgsType()
|
||||
export default class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
identifier: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@ -27,8 +27,6 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
@ -84,7 +82,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'wrong@email.com',
|
||||
identifier: 'wrong@email.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -112,22 +110,20 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'stephen@hawking.uk',
|
||||
identifier: 'stephen@hawking.uk',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account was deleted')],
|
||||
errors: [new GraphQLError('No user to given contact')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail('stephen@hawking.uk')
|
||||
expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk')
|
||||
})
|
||||
})
|
||||
|
||||
@ -143,22 +139,23 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'garrick@ollivander.com',
|
||||
identifier: 'garrick@ollivander.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
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 () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail('garrick@ollivander.com')
|
||||
expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No user with this credentials',
|
||||
'garrick@ollivander.com',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -178,7 +175,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'bob@baumeister.de',
|
||||
identifier: 'bob@baumeister.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -202,7 +199,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -226,7 +223,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
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',
|
||||
},
|
||||
@ -250,7 +247,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'testing',
|
||||
},
|
||||
@ -300,7 +297,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
@ -323,7 +320,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 50,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
@ -380,7 +377,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 10,
|
||||
memo: 'first transaction',
|
||||
},
|
||||
@ -396,7 +393,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 20,
|
||||
memo: 'second transaction',
|
||||
},
|
||||
@ -412,7 +409,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 30,
|
||||
memo: 'third transaction',
|
||||
},
|
||||
@ -428,7 +425,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 40,
|
||||
memo: 'fourth transaction',
|
||||
},
|
||||
|
||||
@ -35,7 +35,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
export const executeTransaction = async (
|
||||
@ -149,7 +149,6 @@ export const executeTransaction = async (
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.debug(`prepare Email for transaction received...`)
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
@ -299,10 +298,10 @@ export class TransactionResolver {
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@Mutation(() => Boolean)
|
||||
async sendCoins(
|
||||
@Args() { email, amount, memo }: TransactionSendArgs,
|
||||
@Args() { identifier, amount, memo }: TransactionSendArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||
logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`)
|
||||
if (amount.lte(0)) {
|
||||
throw new LogError('Amount to send must be positive', amount)
|
||||
}
|
||||
@ -311,13 +310,9 @@ export class TransactionResolver {
|
||||
const senderUser = getUser(context)
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
if (recipientUser.deletedAt) {
|
||||
throw new LogError('The recipient account was deleted', recipientUser)
|
||||
}
|
||||
const emailContact = recipientUser.emailContact
|
||||
if (!emailContact.emailChecked) {
|
||||
throw new LogError('The recipient account is not activated', recipientUser)
|
||||
const recipientUser = await findUserByIdentifier(identifier)
|
||||
if (!recipientUser) {
|
||||
throw new LogError('The recipient user was not found', recipientUser)
|
||||
}
|
||||
|
||||
await executeTransaction(amount, memo, senderUser, recipientUser)
|
||||
|
||||
@ -11,7 +11,7 @@ import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
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 { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
@ -46,7 +46,13 @@ import {
|
||||
unDeleteUser,
|
||||
sendActivationEmail,
|
||||
} 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 { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
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', () => {
|
||||
|
||||
@ -72,6 +72,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
|
||||
const random = require('random-bigint')
|
||||
@ -819,6 +820,12 @@ export class UserResolver {
|
||||
|
||||
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> {
|
||||
|
||||
@ -152,7 +152,7 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bibisTransaction = mutate({
|
||||
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({
|
||||
mutation: login,
|
||||
@ -168,7 +168,7 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bobsTransaction = mutate({
|
||||
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({
|
||||
mutation: login,
|
||||
|
||||
36
backend/src/graphql/resolver/util/findUserByIdentifier.ts
Normal file
36
backend/src/graphql/resolver/util/findUserByIdentifier.ts
Normal 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
|
||||
}
|
||||
@ -75,8 +75,8 @@ export const sendActivationEmail = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -340,3 +340,12 @@ export const listContributionMessages = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const user = gql`
|
||||
query ($identifier: String!) {
|
||||
user(identifier: $identifier) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="2"></b-col>
|
||||
<b-col>
|
||||
<div class="h4">
|
||||
{{ email }}
|
||||
</div>
|
||||
<div class="h4">{{ userName ? userName : identifier }}</div>
|
||||
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
</b-col>
|
||||
@ -64,9 +62,10 @@ export default {
|
||||
name: 'TransactionConfirmationSend',
|
||||
props: {
|
||||
balance: { type: Number, required: true },
|
||||
email: { type: String, required: false, default: '' },
|
||||
identifier: { type: String, required: false, default: '' },
|
||||
amount: { type: Number, required: true },
|
||||
memo: { type: String, required: true },
|
||||
userName: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -2,9 +2,17 @@ import { mount } from '@vue/test-utils'
|
||||
import TransactionForm from './TransactionForm'
|
||||
import flushPromises from 'flush-promises'
|
||||
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
|
||||
localVue.use(VueApollo)
|
||||
|
||||
describe('TransactionForm', () => {
|
||||
let wrapper
|
||||
@ -22,6 +30,7 @@ describe('TransactionForm', () => {
|
||||
},
|
||||
$route: {
|
||||
params: {},
|
||||
query: {},
|
||||
},
|
||||
}
|
||||
|
||||
@ -34,10 +43,24 @@ describe('TransactionForm', () => {
|
||||
localVue,
|
||||
mocks,
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
@ -139,7 +162,7 @@ describe('TransactionForm', () => {
|
||||
.setValue(' valid@email.com ')
|
||||
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur')
|
||||
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')
|
||||
.setValue('Long enough')
|
||||
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.memo).toBe('Long enough')
|
||||
await wrapper.find('button[type="reset"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('')
|
||||
expect(wrapper.vm.form.identifier).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
})
|
||||
@ -321,10 +344,11 @@ Die ganze Welt bezwingen.“`)
|
||||
expect(wrapper.emitted('set-transaction')).toEqual([
|
||||
[
|
||||
{
|
||||
email: 'someone@watches.tv',
|
||||
identifier: 'someone@watches.tv',
|
||||
amount: 87.23,
|
||||
memo: 'Long enough',
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -50,16 +50,24 @@
|
||||
<b-col>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<div v-if="radioSelected === sendTypes.send && !gradidoID">
|
||||
<input-email
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
v-model="form.email"
|
||||
v-model="form.identifier"
|
||||
:disabled="isBalanceDisabled"
|
||||
@onValidation="onValidation"
|
||||
/>
|
||||
</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 cols="12" lg="6">
|
||||
<input-amount
|
||||
@ -121,6 +129,7 @@ import { SEND_TYPES } from '@/pages/Send'
|
||||
import InputEmail from '@/components/Inputs/InputEmail'
|
||||
import InputAmount from '@/components/Inputs/InputAmount'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'TransactionForm',
|
||||
@ -131,20 +140,20 @@ export default {
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
email: { type: String, default: '' },
|
||||
identifier: { type: String, default: '' },
|
||||
amount: { type: Number, default: 0 },
|
||||
memo: { type: String, default: '' },
|
||||
selected: { type: String, default: 'send' },
|
||||
},
|
||||
inject: ['getTunneledEmail'],
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
email: this.email,
|
||||
identifier: this.identifier,
|
||||
amount: this.amount ? String(this.amount) : '',
|
||||
memo: this.memo,
|
||||
},
|
||||
radioSelected: this.selected,
|
||||
userName: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -152,33 +161,48 @@ export default {
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
onSubmit() {
|
||||
if (this.gradidoID) this.form.identifier = this.gradidoID
|
||||
this.$emit('set-transaction', {
|
||||
selected: this.radioSelected,
|
||||
email: this.form.email,
|
||||
identifier: this.form.identifier,
|
||||
amount: Number(this.form.amount.replace(',', '.')),
|
||||
memo: this.form.memo,
|
||||
userName: this.userName,
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
event.preventDefault()
|
||||
this.form.email = ''
|
||||
this.form.identifier = ''
|
||||
this.form.amount = ''
|
||||
this.form.memo = ''
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
setNewRecipientEmail() {
|
||||
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
|
||||
if (this.$route.query && !this.$route.query === {}) this.$router.replace({ query: undefined })
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
recipientEmail() {
|
||||
this.setNewRecipientEmail()
|
||||
apollo: {
|
||||
UserName: {
|
||||
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: {
|
||||
disabled() {
|
||||
if (
|
||||
this.form.email.length > 5 &&
|
||||
this.form.identifier.length > 5 &&
|
||||
parseInt(this.form.amount) <= parseInt(this.balance) &&
|
||||
this.form.memo.length > 5 &&
|
||||
this.form.memo.length <= 255
|
||||
@ -193,15 +217,12 @@ export default {
|
||||
sendTypes() {
|
||||
return SEND_TYPES
|
||||
},
|
||||
recipientEmail() {
|
||||
return this.getTunneledEmail()
|
||||
gradidoID() {
|
||||
return this.$route.query && this.$route.query.gradidoID
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setNewRecipientEmail()
|
||||
},
|
||||
mounted() {
|
||||
if (this.form.email !== '') this.$refs.formValidator.validate()
|
||||
if (this.form.identifier !== '') this.$refs.formValidator.validate()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -37,7 +37,6 @@
|
||||
<transaction-send
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -45,7 +44,6 @@
|
||||
<transaction-receive
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -53,7 +51,6 @@
|
||||
<transaction-creation
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@ -32,11 +32,7 @@
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="font-weight-bold">
|
||||
<name
|
||||
:linkedUser="transaction.linkedUser"
|
||||
v-on="$listeners"
|
||||
fontColor="text-dark"
|
||||
/>
|
||||
<name :linkedUser="transaction.linkedUser" fontColor="text-dark" />
|
||||
</div>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="small">
|
||||
|
||||
@ -3,9 +3,11 @@ import Name from './Name'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$router: {
|
||||
push: jest.fn(),
|
||||
push: routerPushMock,
|
||||
history: {
|
||||
current: {
|
||||
fullPath: '/transactions',
|
||||
@ -47,7 +49,7 @@ describe('Name', () => {
|
||||
describe('with linked user', () => {
|
||||
beforeEach(async () => {
|
||||
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')
|
||||
})
|
||||
|
||||
it('emits set tunneled email', () => {
|
||||
expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']])
|
||||
it('pushes router to send', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
path: '/send',
|
||||
})
|
||||
})
|
||||
|
||||
it('pushes the route with query for email', () => {
|
||||
expect(mocks.$router.push).toBeCalledWith({
|
||||
path: '/send',
|
||||
it('pushes query for gradidoID', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
query: {
|
||||
gradidoID: 'gradido-ID',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="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">
|
||||
{{ itemText }}
|
||||
</b-link>
|
||||
@ -35,8 +35,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
tunnelEmail() {
|
||||
this.$emit('set-tunneled-email', this.linkedUser.email)
|
||||
if (this.$router.history.current.fullPath !== '/send') this.$router.push({ path: '/send' })
|
||||
this.$router.push({ query: { gradidoID: this.linkedUser.gradidoID } })
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
<div>
|
||||
<name
|
||||
class="font-weight-bold"
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:linkId="linkId"
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
<div>
|
||||
<name
|
||||
class="font-weight-bold"
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:linkId="linkId"
|
||||
|
||||
@ -69,8 +69,8 @@ export const createUser = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
mutation($identifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
|
||||
}
|
||||
`
|
||||
|
||||
@ -144,6 +144,7 @@ export const createContributionMessage = gql`
|
||||
export const login = gql`
|
||||
mutation($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
gradidoID
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
|
||||
@ -38,6 +38,7 @@ export const transactionsQuery = gql`
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
gradidoID
|
||||
email
|
||||
}
|
||||
decay {
|
||||
@ -268,3 +269,12 @@ export const openCreations = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const user = gql`
|
||||
query($identifier: String!) {
|
||||
user(identifier: $identifier) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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', () => {
|
||||
expect(wrapper.findComponent({ name: 'Navbar' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
@ -127,7 +127,6 @@
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
</template>
|
||||
<template #community>
|
||||
@ -149,7 +148,6 @@
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
:pending="pending"
|
||||
@update-transactions="updateTransactions"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
></router-view>
|
||||
</fade-transition>
|
||||
</div>
|
||||
@ -164,7 +162,6 @@
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
</template>
|
||||
<template #empty />
|
||||
@ -234,18 +231,12 @@ export default {
|
||||
transactionLinkCount: 0,
|
||||
pending: true,
|
||||
visible: false,
|
||||
tunneledEmail: null,
|
||||
hamburger: true,
|
||||
darkMode: false,
|
||||
skeleton: true,
|
||||
totalUsers: null,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getTunneledEmail: () => this.tunneledEmail,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateTransactions(0)
|
||||
this.getCommunityStatistics()
|
||||
@ -319,9 +310,6 @@ export default {
|
||||
setVisible(bool) {
|
||||
this.visible = bool
|
||||
},
|
||||
setTunneledEmail(email) {
|
||||
this.tunneledEmail = email
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -10,6 +10,7 @@ const apolloMutationMock = jest.fn()
|
||||
apolloMutationMock.mockResolvedValue('success')
|
||||
|
||||
const navigatorClipboardMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -38,6 +39,9 @@ describe('Send', () => {
|
||||
$route: {
|
||||
query: {},
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
@ -85,8 +89,8 @@ describe('Send', () => {
|
||||
it('shows the transaction formular again', () => {
|
||||
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='text']").vm.$el.value).toBe('23.45')
|
||||
expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!')
|
||||
@ -107,10 +111,11 @@ describe('Send', () => {
|
||||
expect.objectContaining({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'user@example.org',
|
||||
identifier: 'user@example.org',
|
||||
amount: 23.45,
|
||||
memo: 'Make the best of it!',
|
||||
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', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -11,9 +11,7 @@
|
||||
<template #transactionConfirmationSend>
|
||||
<transaction-confirmation-send
|
||||
:balance="balance"
|
||||
:email="transactionData.email"
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
v-bind="transactionData"
|
||||
@send-transaction="sendTransaction"
|
||||
@on-back="onBack"
|
||||
></transaction-confirmation-send>
|
||||
@ -21,7 +19,7 @@
|
||||
<template #transactionConfirmationLink>
|
||||
<transaction-confirmation-link
|
||||
:balance="balance"
|
||||
:email="transactionData.email"
|
||||
:email="transactionData.identifier"
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
:loading="loading"
|
||||
@ -62,7 +60,7 @@ import TransactionResultLink from '@/components/GddSend/TransactionResultLink'
|
||||
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
|
||||
|
||||
const EMPTY_TRANSACTION_DATA = {
|
||||
email: '',
|
||||
identifier: '',
|
||||
amount: 0,
|
||||
memo: '',
|
||||
}
|
||||
@ -168,6 +166,7 @@ export default {
|
||||
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
|
||||
}
|
||||
this.loading = false
|
||||
this.$router.push({ query: { gradidoID: undefined } })
|
||||
},
|
||||
onBack() {
|
||||
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
:showPagination="true"
|
||||
:pageSize="pageSize"
|
||||
@update-transactions="updateTransactions"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user