mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge remote-tracking branch 'origin/master' into
2947-refactor-the-existing-sendcoins-resolver-methode-to-distingue-between-local-transaction-and-x-transaction
This commit is contained in:
commit
04e7a92270
@ -89,7 +89,6 @@
|
||||
"submit": "Senden"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"hashtag_symbol": "#",
|
||||
"help": {
|
||||
"help": "Hilfe",
|
||||
"transactionlist": {
|
||||
@ -125,10 +124,8 @@
|
||||
"user_search": "Nutzersuche"
|
||||
},
|
||||
"not_open_creations": "Keine offenen Schöpfungen",
|
||||
"no_filter": "Keine Filterung",
|
||||
"no_filter_tooltip": "Es wird nicht nach Hashtags gefiltert",
|
||||
"no_hashtag": "Ohne Hashtag",
|
||||
"no_hashtag_tooltip": "Zeigt nur Schöpfungen ohne Hashtag im Kommentar an",
|
||||
"no_hashtag": "#Hashtags verbergen",
|
||||
"no_hashtag_tooltip": "Zeigt nur Beiträge ohne Hashtag im Text",
|
||||
"open": "offen",
|
||||
"open_creations": "Offene Schöpfungen",
|
||||
"overlay": {
|
||||
@ -221,7 +218,7 @@
|
||||
"tabTitle": "Nutzer-Rolle"
|
||||
},
|
||||
"user_deleted": "Nutzer ist gelöscht.",
|
||||
"user_memo_search": "Nutzer-Kommentar-Suche",
|
||||
"user_memo_search": "Benutzer- und Text-Suche",
|
||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||
"user_search": "Nutzer-Suche"
|
||||
}
|
||||
|
||||
@ -89,7 +89,6 @@
|
||||
"submit": "Send"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"hashtag_symbol": "#",
|
||||
"help": {
|
||||
"help": "Help",
|
||||
"transactionlist": {
|
||||
@ -125,10 +124,8 @@
|
||||
"user_search": "User search"
|
||||
},
|
||||
"not_open_creations": "No open creations",
|
||||
"no_filter": "No Filter",
|
||||
"no_filter_tooltip": "It is not filtered by hashtags",
|
||||
"no_hashtag": "No Hashtag",
|
||||
"no_hashtag_tooltip": "Displays only contributions without hashtag in comment",
|
||||
"no_hashtag": "Hide #hashtags",
|
||||
"no_hashtag_tooltip": "Shows only contributions without hashtag in text",
|
||||
"open": "open",
|
||||
"open_creations": "Open creations",
|
||||
"overlay": {
|
||||
@ -221,7 +218,7 @@
|
||||
"tabTitle": "User Role"
|
||||
},
|
||||
"user_deleted": "User is deleted.",
|
||||
"user_memo_search": "User and Memo search",
|
||||
"user_memo_search": "User and text search",
|
||||
"user_recovered": "User is recovered.",
|
||||
"user_search": "User search"
|
||||
}
|
||||
|
||||
@ -2,12 +2,10 @@
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<user-query class="mb-2 mt-2" v-model="query" :placeholder="$t('user_memo_search')" />
|
||||
<div class="mb-4">
|
||||
<b-button class="noHashtag" variant="light" @click="swapNoHashtag" v-b-tooltip="tooltipText">
|
||||
<span :style="hashtagColor">{{ $t('hashtag_symbol') }}</span>
|
||||
{{ noHashtag ? $t('no_hashtag') : $t('no_filter') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<label class="mb-4">
|
||||
<input type="checkbox" class="noHashtag" v-model="noHashtag" @change="swapNoHashtag" />
|
||||
<span class="ml-2" v-b-tooltip="$t('no_hashtag_tooltip')">{{ $t('no_hashtag') }}</span>
|
||||
</label>
|
||||
<div>
|
||||
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
|
||||
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
|
||||
@ -134,7 +132,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
swapNoHashtag() {
|
||||
this.noHashtag = !!(this.noHashtag === null || this.noHashtag === false)
|
||||
this.query()
|
||||
},
|
||||
deleteCreation() {
|
||||
@ -209,12 +206,6 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hashtagColor() {
|
||||
return this.noHashtag ? 'color: red' : 'color: black'
|
||||
},
|
||||
tooltipText() {
|
||||
return this.noHashtag ? this.$t('no_hashtag_tooltip') : this.$t('no_filter_tooltip')
|
||||
},
|
||||
fields() {
|
||||
return [
|
||||
[
|
||||
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 90,
|
||||
lines: 89,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
@ -20,6 +20,7 @@ module.exports = {
|
||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
|
||||
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
|
||||
'@typeorm/(.*)': '<rootDir>/src/typeorm/$1',
|
||||
'@test/(.*)': '<rootDir>/test/$1',
|
||||
'@entity/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
|
||||
@ -12,7 +12,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0070-add_dlt_transactions_table',
|
||||
DB_VERSION: '0071-add-pending_transactions-table',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicCommunityInfo'
|
||||
import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { PublicCommunityInfo } from './model/PublicCommunityInfo'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export class FederationClient {
|
||||
dbCom: DbFederatedCommunity
|
||||
@ -46,4 +49,27 @@ export class FederationClient {
|
||||
logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
getPublicCommunityInfo = async (): Promise<PublicCommunityInfo | undefined> => {
|
||||
logger.debug(`Federation: getPublicCommunityInfo with endpoint='${this.endpoint}'...`)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(getPublicCommunityInfo, {})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.getPublicCommunityInfo?.name) {
|
||||
logger.warn(
|
||||
'Federation: getPublicCommunityInfo without response data from endpoint',
|
||||
this.endpoint,
|
||||
)
|
||||
return
|
||||
}
|
||||
logger.debug(`Federation: getPublicCommunityInfo successful from endpoint=${this.endpoint}`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
logger.debug(`publicCommunityInfo:`, data.getPublicCommunityInfo)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.getPublicCommunityInfo
|
||||
} catch (err) {
|
||||
logger.warn('Federation: getPublicCommunityInfo failed for endpoint', this.endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export interface PublicCommunityInfo {
|
||||
name: string
|
||||
description: string
|
||||
creationDate: Date
|
||||
publicKey: string
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const getPublicCommunityInfo = gql`
|
||||
query {
|
||||
getPublicCommunityInfo {
|
||||
name
|
||||
description
|
||||
creationDate
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -59,6 +59,44 @@ describe('validate Communities', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
|
||||
})
|
||||
|
||||
describe('with one Community of api 1_0 but missing pubKey response', () => {
|
||||
beforeEach(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return { data: {} } as Response<unknown>
|
||||
})
|
||||
const variables1 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables1)
|
||||
.orUpdate({
|
||||
// eslint-disable-next-line camelcase
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
jest.clearAllMocks()
|
||||
await validateCommunities()
|
||||
})
|
||||
|
||||
it('logs one community found', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
|
||||
})
|
||||
it('logs requestGetPublicKey missing response data ', () => {
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
'Federation: getPublicKey without response data from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with one Community of api 1_0 and not matching pubKey', () => {
|
||||
beforeEach(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@ -88,7 +126,37 @@ describe('validate Communities', () => {
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
|
||||
/*
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
data: {
|
||||
getPublicCommunityInfo: {
|
||||
name: 'Test-Community',
|
||||
description: 'Description of Test-Community',
|
||||
createdAt: 'someDate',
|
||||
publicKey: 'somePubKey',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
const variables2 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables1)
|
||||
.orUpdate({
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
*/
|
||||
jest.clearAllMocks()
|
||||
await validateCommunities()
|
||||
})
|
||||
@ -155,10 +223,26 @@ describe('validate Communities', () => {
|
||||
})
|
||||
it('logs community pubKey verified', () => {
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
'Federation: verified community with',
|
||||
'http//localhost:5001/api/',
|
||||
5,
|
||||
'Federation: getPublicKey successful from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
'11111111111111111111111111111111',
|
||||
)
|
||||
/*
|
||||
await expect(DbCommunity.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
foreign: false,
|
||||
url: 'http://localhost/api',
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
privateKey: expect.any(Buffer),
|
||||
communityUuid: expect.any(String),
|
||||
authenticatedAt: expect.any(Date),
|
||||
name: expect.any(String),
|
||||
description: expect.any(String),
|
||||
creationDate: expect.any(Date),
|
||||
}),
|
||||
)
|
||||
*/
|
||||
})
|
||||
})
|
||||
describe('with two Communities of api 1_0 and 1_1', () => {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
/** eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/** eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/** eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { IsNull } from '@dbTools/typeorm'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
|
||||
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
|
||||
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
@ -48,7 +50,14 @@ export async function validateCommunities(): Promise<void> {
|
||||
const pubKey = await client.getPublicKey()
|
||||
if (pubKey && pubKey === dbCom.publicKey.toString()) {
|
||||
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
|
||||
logger.debug('Federation: verified community with', dbCom.endPoint)
|
||||
logger.info(`Federation: verified community with:`, dbCom.endPoint)
|
||||
const pubComInfo = await client.getPublicCommunityInfo()
|
||||
if (pubComInfo) {
|
||||
await writeForeignCommunity(dbCom, pubComInfo)
|
||||
logger.info(`Federation: write publicInfo of community: name=${pubComInfo.name}`)
|
||||
} else {
|
||||
logger.warn('Federation: missing result of getPublicCommunityInfo')
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
'Federation: received not matching publicKey:',
|
||||
@ -62,3 +71,28 @@ export async function validateCommunities(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function writeForeignCommunity(
|
||||
dbCom: DbFederatedCommunity,
|
||||
pubInfo: PublicCommunityInfo,
|
||||
): Promise<void> {
|
||||
if (!dbCom || !pubInfo || !(dbCom.publicKey.toString() === pubInfo.publicKey)) {
|
||||
logger.error(
|
||||
`Error in writeForeignCommunity: missmatching parameters or publicKey. pubInfo:${JSON.stringify(
|
||||
pubInfo,
|
||||
)}`,
|
||||
)
|
||||
} else {
|
||||
let com = await DbCommunity.findOneBy({ publicKey: dbCom.publicKey })
|
||||
if (!com) {
|
||||
com = DbCommunity.create()
|
||||
}
|
||||
com.creationDate = pubInfo.creationDate
|
||||
com.description = pubInfo.description
|
||||
com.foreign = true
|
||||
com.name = pubInfo.name
|
||||
com.publicKey = dbCom.publicKey
|
||||
com.url = dbCom.endPoint
|
||||
await DbCommunity.save(com)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,28 @@
|
||||
import { IsEmail, MaxLength, MinLength } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
|
||||
import { isValidDateString } from '@/graphql/validator/DateString'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export class AdminCreateContributionArgs {
|
||||
@Field(() => String)
|
||||
@IsEmail()
|
||||
email: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
creationDate: string
|
||||
}
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
import { IsPositive, MaxLength, MinLength } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
|
||||
import { isValidDateString } from '@/graphql/validator/DateString'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@ArgsType()
|
||||
export class AdminUpdateContributionArgs {
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
id: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
creationDate: string
|
||||
}
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
import { MaxLength, MinLength } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
|
||||
import { isValidDateString } from '@/graphql/validator/DateString'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export class ContributionArgs {
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
creationDate: string
|
||||
}
|
||||
|
||||
@ -1,29 +1,49 @@
|
||||
import { IsPositive, IsString, MaxLength, MinLength } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
import {
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||
} from '@/graphql/resolver/const/const'
|
||||
import { isValidDateString } from '@/graphql/validator/DateString'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@ArgsType()
|
||||
export class ContributionLinkArgs {
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(CONTRIBUTIONLINK_NAME_MAX_CHARS)
|
||||
@MinLength(CONTRIBUTIONLINK_NAME_MIN_CHARS)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
cycle: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
validFrom?: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
validTo?: string | null
|
||||
|
||||
@Field(() => Decimal, { nullable: true })
|
||||
@IsPositiveDecimal()
|
||||
maxAmountPerMonth?: Decimal | null
|
||||
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
maxPerCycle: number
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { IsInt, IsString, IsEnum } from 'class-validator'
|
||||
import { ArgsType, Field, Int, InputType } from 'type-graphql'
|
||||
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
@ -6,11 +7,14 @@ import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
@ArgsType()
|
||||
export class ContributionMessageArgs {
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
contributionId: number
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
message: string
|
||||
|
||||
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
|
||||
@IsEnum(ContributionMessageType)
|
||||
messageType: ContributionMessageType
|
||||
}
|
||||
|
||||
@ -1,25 +1,33 @@
|
||||
import { IsEmail, IsInt, IsString } from 'class-validator'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class CreateUserArgs {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
alias?: string | null
|
||||
|
||||
@Field(() => String)
|
||||
@IsEmail()
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
firstName: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
lastName: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
language?: string | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsInt()
|
||||
publisherId?: number | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
redeemCode?: string | null
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable type-graphql/invalid-nullable-input-type */
|
||||
import { IsPositive, IsEnum } from 'class-validator'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { Order } from '@enum/Order'
|
||||
@ -6,11 +7,14 @@ import { Order } from '@enum/Order'
|
||||
@ArgsType()
|
||||
export class Paginated {
|
||||
@Field(() => Int, { defaultValue: 1 })
|
||||
@IsPositive()
|
||||
currentPage: number
|
||||
|
||||
@Field(() => Int, { defaultValue: 3 })
|
||||
@IsPositive()
|
||||
pageSize: number
|
||||
|
||||
@Field(() => Order, { defaultValue: Order.DESC })
|
||||
@IsEnum(Order)
|
||||
order: Order
|
||||
}
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
import { IsBoolean, IsPositive, IsString } from 'class-validator'
|
||||
import { Field, ArgsType, Int } from 'type-graphql'
|
||||
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
|
||||
import { isContributionStatusArray } from '@/graphql/validator/ContributionStatusArray'
|
||||
|
||||
@ArgsType()
|
||||
export class SearchContributionsFilterArgs {
|
||||
@Field(() => [ContributionStatus], { nullable: true, defaultValue: null })
|
||||
@isContributionStatusArray()
|
||||
statusFilter?: ContributionStatus[] | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsPositive()
|
||||
userId?: number | null
|
||||
|
||||
@Field(() => String, { nullable: true, defaultValue: '' })
|
||||
@IsString()
|
||||
query?: string | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
noHashtag?: boolean | null
|
||||
}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { IsBoolean } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class SearchUsersFilters {
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: null })
|
||||
@IsBoolean()
|
||||
byActivated?: boolean | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: null })
|
||||
@IsBoolean()
|
||||
byDeleted?: boolean | null
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { IsPositive, IsEnum } from 'class-validator'
|
||||
import { ArgsType, Field, Int, InputType } from 'type-graphql'
|
||||
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
@ -6,8 +7,10 @@ import { RoleNames } from '@enum/RoleNames'
|
||||
@ArgsType()
|
||||
export class SetUserRoleArgs {
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
userId: number
|
||||
|
||||
@Field(() => RoleNames, { nullable: true })
|
||||
@IsEnum(RoleNames)
|
||||
role: RoleNames | null | undefined
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { MaxLength, MinLength } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@ArgsType()
|
||||
export class TransactionLinkArgs {
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
/* eslint-disable type-graphql/invalid-nullable-input-type */
|
||||
import { IsBoolean } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class TransactionLinkFilters {
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
withDeleted?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
withExpired?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
withRedeemed?: boolean
|
||||
}
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
import { MaxLength, MinLength, IsString } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql/resolver/const/const'
|
||||
import { IsPositiveDecimal } from '@/graphql/validator/Decimal'
|
||||
|
||||
@ArgsType()
|
||||
export class TransactionSendArgs {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
recipientCommunityIdentifier?: string | null | undefined
|
||||
|
||||
@Field(() => String)
|
||||
identifier: string
|
||||
@IsString()
|
||||
recipientIdentifier: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { IsEmail, IsInt, IsString } from 'class-validator'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class UnsecureLoginArgs {
|
||||
@Field(() => String)
|
||||
@IsEmail()
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
password: string
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsInt()
|
||||
publisherId?: number | null
|
||||
}
|
||||
|
||||
@ -1,31 +1,41 @@
|
||||
import { IsBoolean, IsInt, IsString } from 'class-validator'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class UpdateUserInfosArgs {
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
firstName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
lastName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
alias?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
language?: string
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsInt()
|
||||
publisherId?: number | null
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
password?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
passwordNew?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
hideAmountGDD?: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
hideAmountGDT?: boolean
|
||||
}
|
||||
|
||||
14
backend/src/graphql/enum/PendingTransactionState.ts
Normal file
14
backend/src/graphql/enum/PendingTransactionState.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum PendingTransactionState {
|
||||
NEW = 1,
|
||||
WAIT_ON_PENDING = 2,
|
||||
PENDING = 3,
|
||||
WAIT_ON_CONFIRM = 4,
|
||||
CONFIRMED = 5,
|
||||
}
|
||||
|
||||
registerEnumType(PendingTransactionState, {
|
||||
name: 'PendingTransactionState', // this one is mandatory
|
||||
description: 'State of the PendingTransaction', // this one is optional
|
||||
})
|
||||
@ -6,12 +6,13 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
|
||||
import { testEnvironment } from '@test/helpers'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
|
||||
import { getCommunities } from '@/seeds/graphql/queries'
|
||||
import { getCommunities, communities } from '@/seeds/graphql/queries'
|
||||
|
||||
// to do: We need a setup for the tests that closes the connection
|
||||
let query: ApolloServerTestClient['query'], con: Connection
|
||||
@ -29,6 +30,7 @@ beforeAll(async () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
@ -55,6 +57,7 @@ describe('CommunityResolver', () => {
|
||||
|
||||
describe('only home-communities entries', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDB()
|
||||
jest.clearAllMocks()
|
||||
|
||||
homeCom1 = DbFederatedCommunity.create()
|
||||
@ -230,4 +233,147 @@ describe('CommunityResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('communities', () => {
|
||||
let homeCom1: DbCommunity
|
||||
let foreignCom1: DbCommunity
|
||||
let foreignCom2: DbCommunity
|
||||
|
||||
describe('with empty list', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDB()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns no community entry', async () => {
|
||||
// const result: Community[] = await query({ query: getCommunities })
|
||||
// expect(result.length).toEqual(0)
|
||||
await expect(query({ query: communities })).resolves.toMatchObject({
|
||||
data: {
|
||||
communities: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with one home-community entry', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDB()
|
||||
jest.clearAllMocks()
|
||||
|
||||
homeCom1 = DbCommunity.create()
|
||||
homeCom1.foreign = false
|
||||
homeCom1.url = 'http://localhost/api'
|
||||
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
|
||||
homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity')
|
||||
homeCom1.communityUuid = 'HomeCom-UUID'
|
||||
homeCom1.authenticatedAt = new Date()
|
||||
homeCom1.name = 'HomeCommunity-name'
|
||||
homeCom1.description = 'HomeCommunity-description'
|
||||
homeCom1.creationDate = new Date()
|
||||
await DbCommunity.insert(homeCom1)
|
||||
})
|
||||
|
||||
it('returns 1 home-community entry', async () => {
|
||||
await expect(query({ query: communities })).resolves.toMatchObject({
|
||||
data: {
|
||||
communities: [
|
||||
{
|
||||
id: expect.any(Number),
|
||||
foreign: homeCom1.foreign,
|
||||
name: homeCom1.name,
|
||||
description: homeCom1.description,
|
||||
url: homeCom1.url,
|
||||
creationDate: homeCom1.creationDate?.toISOString(),
|
||||
uuid: homeCom1.communityUuid,
|
||||
authenticatedAt: homeCom1.authenticatedAt?.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with several community entries', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDB()
|
||||
jest.clearAllMocks()
|
||||
|
||||
homeCom1 = DbCommunity.create()
|
||||
homeCom1.foreign = false
|
||||
homeCom1.url = 'http://localhost/api'
|
||||
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
|
||||
homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity')
|
||||
homeCom1.communityUuid = 'HomeCom-UUID'
|
||||
homeCom1.authenticatedAt = new Date()
|
||||
homeCom1.name = 'HomeCommunity-name'
|
||||
homeCom1.description = 'HomeCommunity-description'
|
||||
homeCom1.creationDate = new Date()
|
||||
await DbCommunity.insert(homeCom1)
|
||||
|
||||
foreignCom1 = DbCommunity.create()
|
||||
foreignCom1.foreign = true
|
||||
foreignCom1.url = 'http://stage-2.gradido.net/api'
|
||||
foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community')
|
||||
foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community')
|
||||
foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
||||
foreignCom1.authenticatedAt = new Date()
|
||||
foreignCom1.name = 'Stage-2_Community-name'
|
||||
foreignCom1.description = 'Stage-2_Community-description'
|
||||
foreignCom1.creationDate = new Date()
|
||||
await DbCommunity.insert(foreignCom1)
|
||||
|
||||
foreignCom2 = DbCommunity.create()
|
||||
foreignCom2.foreign = true
|
||||
foreignCom2.url = 'http://stage-3.gradido.net/api'
|
||||
foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community')
|
||||
foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community')
|
||||
foreignCom2.communityUuid = 'Stage3-Com-UUID'
|
||||
foreignCom2.authenticatedAt = new Date()
|
||||
foreignCom2.name = 'Stage-3_Community-name'
|
||||
foreignCom2.description = 'Stage-3_Community-description'
|
||||
foreignCom2.creationDate = new Date()
|
||||
await DbCommunity.insert(foreignCom2)
|
||||
})
|
||||
|
||||
it('returns 3 community entries', async () => {
|
||||
await expect(query({ query: communities })).resolves.toMatchObject({
|
||||
data: {
|
||||
communities: [
|
||||
{
|
||||
id: expect.any(Number),
|
||||
foreign: homeCom1.foreign,
|
||||
name: homeCom1.name,
|
||||
description: homeCom1.description,
|
||||
url: homeCom1.url,
|
||||
creationDate: homeCom1.creationDate?.toISOString(),
|
||||
uuid: homeCom1.communityUuid,
|
||||
authenticatedAt: homeCom1.authenticatedAt?.toISOString(),
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
foreign: foreignCom1.foreign,
|
||||
name: foreignCom1.name,
|
||||
description: foreignCom1.description,
|
||||
url: foreignCom1.url,
|
||||
creationDate: foreignCom1.creationDate?.toISOString(),
|
||||
uuid: foreignCom1.communityUuid,
|
||||
authenticatedAt: foreignCom1.authenticatedAt?.toISOString(),
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
foreign: foreignCom2.foreign,
|
||||
name: foreignCom2.name,
|
||||
description: foreignCom2.description,
|
||||
url: foreignCom2.url,
|
||||
creationDate: foreignCom2.creationDate?.toISOString(),
|
||||
uuid: foreignCom2.communityUuid,
|
||||
authenticatedAt: foreignCom2.authenticatedAt?.toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -26,7 +26,7 @@ export class CommunityResolver {
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITIES])
|
||||
@Query(() => [Community])
|
||||
async getCommunitySelections(): Promise<Community[]> {
|
||||
async communities(): Promise<Community[]> {
|
||||
const dbCommunities: DbCommunity[] = await DbCommunity.find({
|
||||
order: {
|
||||
name: 'ASC',
|
||||
|
||||
@ -339,107 +339,142 @@ describe('Contribution Links', () => {
|
||||
|
||||
it('returns an error if name is shorter than 5 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '123',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '123',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'name',
|
||||
constraints: {
|
||||
minLength: 'name must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The value of name is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "The value of name is too short"', () => {
|
||||
expect(logger.error).toBeCalledWith('The value of name is too short', 3)
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an error if name is longer than 100 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'name',
|
||||
constraints: {
|
||||
maxLength: 'name must be shorter than or equal to 100 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The value of name is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "The value of name is too long"', () => {
|
||||
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '123',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '123',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
minLength: 'memo must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The value of memo is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "The value of memo is too short"', () => {
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too short', 3)
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an error if memo is longer than 255 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
maxLength: 'memo must be shorter than or equal to 255 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The value of memo is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "The value of memo is too long"', () => {
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too long', 256)
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an error if amount is not positive', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
amount: new Decimal(0),
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
amount: new Decimal(0),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'amount',
|
||||
constraints: {
|
||||
isPositiveDecimal: 'The amount must be a positive value amount',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The amount must be a positiv value')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "The amount must be a positiv value"', () => {
|
||||
expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0))
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { MoreThan, IsNull } from '@dbTools/typeorm'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Resolver, Args, Arg, Authorized, Mutation, Query, Int, Ctx } from 'type-graphql'
|
||||
|
||||
import { ContributionLinkArgs } from '@arg/ContributionLinkArgs'
|
||||
@ -18,12 +17,6 @@ import {
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import {
|
||||
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
} from './const/const'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import { isStartEndDateValid } from './util/creations'
|
||||
|
||||
@ -46,21 +39,6 @@ export class ContributionLinkResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
|
||||
throw new LogError('The value of name is too short', name.length)
|
||||
}
|
||||
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
|
||||
throw new LogError('The value of name is too long', name.length)
|
||||
}
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('The value of memo is too short', memo.length)
|
||||
}
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('The value of memo is too long', memo.length)
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
throw new LogError('The amount must be a positiv value', amount)
|
||||
}
|
||||
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
|
||||
@ -201,6 +201,7 @@ describe('ContributionResolver', () => {
|
||||
it('throws error when memo length smaller than 5 chars', async () => {
|
||||
jest.clearAllMocks()
|
||||
const date = new Date()
|
||||
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
@ -209,12 +210,23 @@ describe('ContributionResolver', () => {
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
|
||||
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')])
|
||||
})
|
||||
|
||||
it('logs the error "Memo text is too short"', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
minLength: 'memo must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws error when memo length greater than 255 chars', async () => {
|
||||
@ -228,11 +240,23 @@ describe('ContributionResolver', () => {
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')])
|
||||
})
|
||||
|
||||
it('logs the error "Memo text is too long"', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
maxLength: 'memo must be shorter than or equal to 255 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
@ -245,27 +269,35 @@ describe('ContributionResolver', () => {
|
||||
creationDate: 'not-valid',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'creationDate',
|
||||
constraints: {
|
||||
isValidDateString: 'creationDate must be a valid date string, creationDate',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('logs the error "No information for available creations for the given date"', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations for the given date',
|
||||
expect.any(Date),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate 3 month behind', async () => {
|
||||
jest.clearAllMocks()
|
||||
const date = new Date()
|
||||
date.setMonth(date.getMonth() - 3)
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([
|
||||
@ -346,11 +378,23 @@ describe('ContributionResolver', () => {
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')])
|
||||
})
|
||||
|
||||
it('logs the error "Memo text is too short"', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
minLength: 'memo must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -367,11 +411,23 @@ describe('ContributionResolver', () => {
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')])
|
||||
})
|
||||
|
||||
it('logs the error "Memo text is too long"', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
maxLength: 'memo must be shorter than or equal to 255 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -551,13 +607,14 @@ describe('ContributionResolver', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const date = new Date()
|
||||
date.setMonth(date.getMonth() - 3)
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: pendingContribution.data.createContribution.id,
|
||||
amount: 10.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||
creationDate: date.toString(),
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toEqual([
|
||||
@ -1979,17 +2036,28 @@ describe('ContributionResolver', () => {
|
||||
describe('date of creation is not a date string', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({ mutation: adminCreateContribution, variables }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('CreationDate is invalid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error "CreationDate is invalid"', () => {
|
||||
expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date')
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: adminCreateContribution,
|
||||
variables,
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'creationDate',
|
||||
constraints: {
|
||||
isValidDateString:
|
||||
'creationDate must be a valid date string, creationDate',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -2181,7 +2249,7 @@ describe('ContributionResolver', () => {
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
variables: {
|
||||
id: -1,
|
||||
id: 728,
|
||||
amount: new Decimal(300),
|
||||
memo: 'Danke Bibi!',
|
||||
creationDate: contributionDateFormatter(new Date()),
|
||||
@ -2195,7 +2263,7 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
it('logs the error "Contribution not found"', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', 728)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2718,22 +2786,58 @@ describe('ContributionResolver', () => {
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: '#firefighters',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns 17 creations in total', async () => {
|
||||
it('throw error for invalid ContributionStatus in statusFilter array', async () => {
|
||||
const { errors: errorObjects } = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
statusFilter: ['INVALID_STATUS'],
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message:
|
||||
'Variable "$statusFilter" got invalid value "INVALID_STATUS" at "statusFilter[0]"; Value "INVALID_STATUS" does not exist in "ContributionStatus" enum.',
|
||||
extensions: {
|
||||
code: 'BAD_USER_INPUT',
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('returns 18 creations in total', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(17)
|
||||
// console.log('17 contributions: %s', JSON.stringify(contributionListObject, null, 2))
|
||||
expect(contributionListObject.contributionList).toHaveLength(18)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 17,
|
||||
contributionCount: 18,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(50),
|
||||
firstName: 'Bibi',
|
||||
@ -2905,8 +3009,17 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(2)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 4,
|
||||
contributionCount: 5,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '400',
|
||||
firstName: 'Peter',
|
||||
@ -2916,15 +3029,6 @@ describe('ContributionResolver', () => {
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
status: 'DENIED',
|
||||
}),
|
||||
@ -2951,6 +3055,60 @@ describe('ContributionResolver', () => {
|
||||
query: 'Peter',
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(4)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 4,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
it('returns only contributions of the queried user without hashtags', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'Peter',
|
||||
noHashtag: true,
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(3)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 3,
|
||||
@ -2986,6 +3144,48 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('returns only contributions with #firefighter', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: '#firefighter',
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(1)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 1,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: '#firefighters',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
it('returns no contributions with #firefighter and no hashtag', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: '#firefighter',
|
||||
noHashtag: true,
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(0)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 0,
|
||||
})
|
||||
})
|
||||
|
||||
// test for case sensitivity and email
|
||||
it('returns only contributions of the queried user email', async () => {
|
||||
const {
|
||||
|
||||
@ -45,12 +45,10 @@ import { calculateDecay } from '@/util/decay'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { fullName } from '@/util/utilities'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import {
|
||||
getUserCreation,
|
||||
validateContribution,
|
||||
updateCreations,
|
||||
isValidDateString,
|
||||
getOpenCreations,
|
||||
} from './util/creations'
|
||||
import { findContributions } from './util/findContributions'
|
||||
@ -66,12 +64,6 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
@ -181,12 +173,6 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
|
||||
@ -264,9 +250,6 @@ export class ContributionResolver {
|
||||
`adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`,
|
||||
)
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (!isValidDateString(creationDate)) {
|
||||
throw new LogError('CreationDate is invalid', creationDate)
|
||||
}
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
|
||||
@ -94,38 +94,115 @@ describe('TransactionLinkResolver', () => {
|
||||
|
||||
it('throws error when amount is zero', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 0,
|
||||
memo: 'Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [new GraphQLError('Amount must be a positive number')],
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 0,
|
||||
memo: 'Test Test',
|
||||
},
|
||||
})
|
||||
})
|
||||
it('logs the error "Amount must be a positive number" - 0', () => {
|
||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'amount',
|
||||
constraints: {
|
||||
isPositiveDecimal: 'The amount must be a positive value amount',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws error when amount is negative', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: -10,
|
||||
memo: 'Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [new GraphQLError('Amount must be a positive number')],
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: -10,
|
||||
memo: 'Test Test',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'amount',
|
||||
constraints: {
|
||||
isPositiveDecimal: 'The amount must be a positive value amount',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
it('logs the error "Amount must be a positive number" - -10', () => {
|
||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
|
||||
|
||||
it('throws error when memo text is too short', async () => {
|
||||
jest.clearAllMocks()
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 100,
|
||||
memo: 'Test',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
minLength: 'memo must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws error when memo text is too long', async () => {
|
||||
jest.clearAllMocks()
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
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',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
maxLength: 'memo must be shorter than or equal to 255 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws error when user has not enough GDD', async () => {
|
||||
@ -135,7 +212,7 @@ describe('TransactionLinkResolver', () => {
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 1001,
|
||||
memo: 'Test',
|
||||
memo: 'Test Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
@ -74,10 +74,6 @@ export class TransactionLinkResolver {
|
||||
const createdDate = new Date()
|
||||
const validUntil = transactionLinkExpireDate(createdDate)
|
||||
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
throw new LogError('Amount must be a positive number', amount)
|
||||
}
|
||||
|
||||
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
|
||||
|
||||
// validate amount
|
||||
|
||||
@ -7,7 +7,6 @@ import { Event as DbEvent } from '@entity/Event'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { User } from '@entity/User'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
@ -92,9 +91,9 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'wrong@email.com',
|
||||
recipientIdentifier: 'wrong@email.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
memo: 'test test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
@ -120,9 +119,9 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'stephen@hawking.uk',
|
||||
recipientIdentifier: 'stephen@hawking.uk',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
memo: 'test test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
@ -149,9 +148,9 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'garrick@ollivander.com',
|
||||
recipientIdentifier: 'garrick@ollivander.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
memo: 'test test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
@ -185,9 +184,9 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'bob@baumeister.de',
|
||||
recipientIdentifier: 'bob@baumeister.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
memo: 'test test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
@ -205,48 +204,62 @@ describe('send coins', () => {
|
||||
describe('memo text is too short', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'Test',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
minLength: 'memo must be longer than or equal to 5 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Memo text is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too long', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
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',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
recipientIdentifier: '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',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'memo',
|
||||
constraints: {
|
||||
maxLength: 'memo must be shorter than or equal to 255 characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Memo text is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 256)
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -257,7 +270,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'testing',
|
||||
},
|
||||
@ -303,24 +316,31 @@ describe('send coins', () => {
|
||||
describe('trying to send negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
const { errors: errorObjects } = await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
})
|
||||
expect(errorObjects).toMatchObject([
|
||||
{
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
exception: {
|
||||
validationErrors: [
|
||||
{
|
||||
property: 'amount',
|
||||
constraints: {
|
||||
isPositiveDecimal: 'The amount must be a positive value amount',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Amount to send must be positive')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Amount to send must be positive', new Decimal(-50))
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -330,7 +350,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 50,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
@ -436,7 +456,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: peter?.gradidoID,
|
||||
recipientIdentifier: peter?.gradidoID,
|
||||
amount: 10,
|
||||
memo: 'send via gradido ID',
|
||||
},
|
||||
@ -476,7 +496,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'bob',
|
||||
recipientIdentifier: 'bob',
|
||||
amount: 6.66,
|
||||
memo: 'send via alias',
|
||||
},
|
||||
@ -544,7 +564,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 10,
|
||||
memo: 'first transaction',
|
||||
},
|
||||
@ -560,7 +580,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 20,
|
||||
memo: 'second transaction',
|
||||
},
|
||||
@ -576,7 +596,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 30,
|
||||
memo: 'third transaction',
|
||||
},
|
||||
@ -592,7 +612,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'peter@lustig.de',
|
||||
recipientIdentifier: 'peter@lustig.de',
|
||||
amount: 40,
|
||||
memo: 'fourth transaction',
|
||||
},
|
||||
|
||||
@ -57,14 +57,6 @@ export const executeTransaction = async (
|
||||
throw new LogError('Sender and Recipient are the same', sender.id)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
@ -470,24 +462,20 @@ export class TransactionResolver {
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@Mutation(() => Boolean)
|
||||
async sendCoins(
|
||||
@Args() { identifier, amount, memo, communityIdentifier }: TransactionSendArgs,
|
||||
@Args()
|
||||
{ recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(
|
||||
`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo}, communityIdentifier=${communityIdentifier})`,
|
||||
logger.debug(
|
||||
`sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, identifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
|
||||
)
|
||||
|
||||
if (amount.lte(0)) {
|
||||
throw new LogError('Amount to send must be positive', amount)
|
||||
}
|
||||
|
||||
const senderUser = getUser(context)
|
||||
|
||||
if (!communityIdentifier || (await isHomeCommunity(communityIdentifier))) {
|
||||
if (!recipientCommunityIdentifier || (await isHomeCommunity(recipientCommunityIdentifier))) {
|
||||
// processing sendCoins within sender and recepient are both in home community
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByIdentifier(identifier)
|
||||
const recipientUser = await findUserByIdentifier(recipientIdentifier)
|
||||
if (!recipientUser) {
|
||||
throw new LogError('The recipient user was not found', recipientUser)
|
||||
}
|
||||
|
||||
@ -156,7 +156,11 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bibisTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { identifier: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
|
||||
variables: {
|
||||
recipientIdentifier: 'bob@baumeister.de',
|
||||
amount: '50',
|
||||
memo: 'Das ist für dich, Bob',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
@ -172,7 +176,11 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bobsTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { identifier: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
|
||||
variables: {
|
||||
recipientIdentifier: 'bibi@bloxberg.de',
|
||||
amount: '50',
|
||||
memo: 'Das ist für dich, Bibi',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
|
||||
@ -2,7 +2,9 @@ import { Community as DbCommunity } from '@entity/Community'
|
||||
|
||||
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
|
||||
const homeCommunity = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
if (communityIdentifier === homeCommunity.name) {
|
||||
if (communityIdentifier === homeCommunity.id.toString()) {
|
||||
return true
|
||||
} else if (communityIdentifier === homeCommunity.name) {
|
||||
return true
|
||||
} else if (communityIdentifier === homeCommunity.communityUuid) {
|
||||
return true
|
||||
|
||||
@ -1,68 +1,69 @@
|
||||
import { In, Like, Not } from '@dbTools/typeorm'
|
||||
/* eslint-disable security/detect-object-injection */
|
||||
import { Brackets, In, Like, Not, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
import { Connection } from '@typeorm/connection'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
interface Relations {
|
||||
[key: string]: boolean | Relations
|
||||
}
|
||||
|
||||
function joinRelationsRecursive(
|
||||
relations: Relations,
|
||||
queryBuilder: SelectQueryBuilder<DbContribution>,
|
||||
currentPath: string,
|
||||
): void {
|
||||
for (const key in relations) {
|
||||
// console.log('leftJoin: %s, %s', `${currentPath}.${key}`, key)
|
||||
queryBuilder.leftJoinAndSelect(`${currentPath}.${key}`, key)
|
||||
if (typeof relations[key] === 'object') {
|
||||
// If it's a nested relation
|
||||
joinRelationsRecursive(relations[key] as Relations, queryBuilder, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const findContributions = async (
|
||||
paginate: Paginated,
|
||||
filter: SearchContributionsFilterArgs,
|
||||
withDeleted = false,
|
||||
relations: Relations | undefined = undefined,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const requiredWhere = {
|
||||
const connection = await Connection.getInstance()
|
||||
if (!connection) {
|
||||
throw new LogError('Cannot connect to db')
|
||||
}
|
||||
const queryBuilder = connection.getRepository(DbContribution).createQueryBuilder('Contribution')
|
||||
if (relations) joinRelationsRecursive(relations, queryBuilder, 'Contribution')
|
||||
if (withDeleted) queryBuilder.withDeleted()
|
||||
queryBuilder.where({
|
||||
...(filter.statusFilter?.length && { contributionStatus: In(filter.statusFilter) }),
|
||||
...(filter.userId && { userId: filter.userId }),
|
||||
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
|
||||
}
|
||||
|
||||
let where =
|
||||
filter.query && relations?.user
|
||||
? [
|
||||
{
|
||||
...requiredWhere, // And
|
||||
user: {
|
||||
firstName: Like(`%${filter.query}%`),
|
||||
},
|
||||
}, // Or
|
||||
{
|
||||
...requiredWhere,
|
||||
user: {
|
||||
lastName: Like(`%${filter.query}%`),
|
||||
},
|
||||
}, // Or
|
||||
{
|
||||
...requiredWhere, // And
|
||||
user: {
|
||||
emailContact: {
|
||||
email: Like(`%${filter.query}%`),
|
||||
},
|
||||
},
|
||||
}, // Or
|
||||
{
|
||||
...requiredWhere, // And
|
||||
memo: Like(`%${filter.query}%`),
|
||||
},
|
||||
]
|
||||
: requiredWhere
|
||||
|
||||
if (!relations?.user && filter.query) {
|
||||
where = [{ ...requiredWhere, memo: Like(`%${filter.query}%`) }]
|
||||
}
|
||||
|
||||
return DbContribution.findAndCount({
|
||||
relations,
|
||||
where,
|
||||
withDeleted,
|
||||
order: {
|
||||
createdAt: paginate.order,
|
||||
id: paginate.order,
|
||||
},
|
||||
skip: (paginate.currentPage - 1) * paginate.pageSize,
|
||||
take: paginate.pageSize,
|
||||
})
|
||||
queryBuilder.printSql()
|
||||
if (filter.query) {
|
||||
const queryString = '%' + filter.query + '%'
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where({ memo: Like(queryString) })
|
||||
if (relations?.user) {
|
||||
qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString })
|
||||
.orWhere('user.last_name LIKE :lastName', { lastName: queryString })
|
||||
.orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString })
|
||||
.orWhere({ memo: Like(queryString) })
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
return queryBuilder
|
||||
.orderBy('Contribution.createdAt', paginate.order)
|
||||
.addOrderBy('Contribution.id', paginate.order)
|
||||
.skip((paginate.currentPage - 1) * paginate.pageSize)
|
||||
.take(paginate.pageSize)
|
||||
.getManyAndCount()
|
||||
}
|
||||
|
||||
@ -12,5 +12,13 @@ export const schema = async (): Promise<GraphQLSchema> => {
|
||||
resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)],
|
||||
authChecker: isAuthorized,
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
validate: {
|
||||
validationError: { target: false },
|
||||
skipMissingProperties: true,
|
||||
skipNullProperties: true,
|
||||
skipUndefinedProperties: false,
|
||||
forbidUnknownValues: true,
|
||||
stopAtFirstError: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
21
backend/src/graphql/validator/ContributionStatusArray.ts
Normal file
21
backend/src/graphql/validator/ContributionStatusArray.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { registerDecorator, ValidationOptions } from 'class-validator'
|
||||
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
|
||||
export function isContributionStatusArray(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isContributionStatusArray',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: ContributionStatus[]): boolean {
|
||||
const validValues = Object.values(ContributionStatus)
|
||||
return value.every((item) => validValues.includes(item))
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
21
backend/src/graphql/validator/DateString.ts
Normal file
21
backend/src/graphql/validator/DateString.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
|
||||
|
||||
export function isValidDateString(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isValidDateString',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: string) {
|
||||
return new Date(value).toString() !== 'Invalid Date'
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${propertyName} must be a valid date string, ${args.property}`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
22
backend/src/graphql/validator/Decimal.ts
Normal file
22
backend/src/graphql/validator/Decimal.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
export function IsPositiveDecimal(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isPositiveDecimal',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: Decimal) {
|
||||
return value.greaterThan(0)
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `The ${propertyName} must be a positive value ${args.property}`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -136,6 +136,14 @@ export const creations: CreationInterface[] = [
|
||||
confirmed: true,
|
||||
moveCreationDate: 12,
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: '#Hexen',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
moveCreationDate: 12,
|
||||
},
|
||||
...bobsTransactions,
|
||||
{
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
|
||||
@ -78,6 +78,7 @@ export const sendActivationEmail = gql`
|
||||
}
|
||||
`
|
||||
|
||||
/*
|
||||
export const sendCoins = gql`
|
||||
mutation ($identifier: String!, $amount: Decimal!, $memo: String!, $communityIdentifier: String) {
|
||||
sendCoins(
|
||||
@ -88,6 +89,22 @@ export const sendCoins = gql`
|
||||
)
|
||||
}
|
||||
`
|
||||
*/
|
||||
export const sendCoins = gql`
|
||||
mutation (
|
||||
$recipientCommunityIdentifier: String!
|
||||
$recipientIdentifier: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
) {
|
||||
sendCoins(
|
||||
recipientCommunityIdentifier: $recipientCommunityIdentifier
|
||||
recipientIdentifier: $recipientIdentifier
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
export const createTransactionLink = gql`
|
||||
mutation ($amount: Decimal!, $memo: String!) {
|
||||
|
||||
@ -118,25 +118,17 @@ export const listGDTEntriesQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const communityInfo = gql`
|
||||
query {
|
||||
getCommunityInfo {
|
||||
name
|
||||
description
|
||||
registerUrl
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const communities = gql`
|
||||
query {
|
||||
communities {
|
||||
id
|
||||
foreign
|
||||
name
|
||||
url
|
||||
description
|
||||
registerUrl
|
||||
url
|
||||
creationDate
|
||||
uuid
|
||||
authenticatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -236,6 +228,7 @@ export const adminListContributions = gql`
|
||||
$statusFilter: [ContributionStatus!]
|
||||
$userId: Int
|
||||
$query: String
|
||||
$noHashtag: Boolean
|
||||
) {
|
||||
adminListContributions(
|
||||
currentPage: $currentPage
|
||||
@ -244,6 +237,7 @@ export const adminListContributions = gql`
|
||||
statusFilter: $statusFilter
|
||||
userId: $userId
|
||||
query: $query
|
||||
noHashtag: $noHashtag
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
"@model/*": ["src/graphql/model/*"],
|
||||
"@union/*": ["src/graphql/union/*"],
|
||||
"@repository/*": ["src/typeorm/repository/*"],
|
||||
"@typeorm/*": ["src/typeorm/*"],
|
||||
"@test/*": ["test/*"],
|
||||
/* external */
|
||||
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||
|
||||
@ -0,0 +1,152 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('pending_transactions')
|
||||
export class PendingTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'state', unsigned: true, nullable: false })
|
||||
state: number
|
||||
|
||||
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
|
||||
previous: number | null
|
||||
|
||||
@Column({ name: 'type_id', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
name: 'transaction_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
transactionLinkId?: number | null
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
balance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: false,
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decay: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'decay_start',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
decayStart: Date | null
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
|
||||
creationDate: Date | null
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({
|
||||
name: 'user_gradido_id',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userGradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'user_name',
|
||||
type: 'varchar',
|
||||
length: 512,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userName: string | null
|
||||
|
||||
@Column({
|
||||
name: 'user_community_uuid',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userCommunityUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedUserId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_gradido_id',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserGradidoID: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_name',
|
||||
type: 'varchar',
|
||||
length: 512,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserName: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_community_uuid',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserCommunityUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'linked_transaction_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedTransactionId?: number | null
|
||||
}
|
||||
1
database/entity/PendingTransaction.ts
Normal file
1
database/entity/PendingTransaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { PendingTransaction } from './0071-add-pending_transactions-table/PendingTransaction'
|
||||
@ -13,6 +13,7 @@ import { Community } from './Community'
|
||||
import { FederatedCommunity } from './FederatedCommunity'
|
||||
import { UserRole } from './UserRole'
|
||||
import { DltTransaction } from './DltTransaction'
|
||||
import { PendingTransaction } from './0071-add-pending_transactions-table/PendingTransaction'
|
||||
|
||||
export const entities = [
|
||||
Community,
|
||||
@ -25,6 +26,7 @@ export const entities = [
|
||||
LoginElopageBuys,
|
||||
LoginEmailOptIn,
|
||||
Migration,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
User,
|
||||
|
||||
@ -39,4 +39,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
|
||||
await queryFn(
|
||||
'ALTER TABLE `communities` MODIFY COLUMN `public_key` binary(64) NULL DEFAULT NULL;',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
36
database/migrations/0071-add-pending_transactions-table.ts
Normal file
36
database/migrations/0071-add-pending_transactions-table.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* MIGRATION TO add new pending_transactions table */
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE pending_transactions (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
state int(10) NOT NULL,
|
||||
previous int(10) unsigned DEFAULT NULL NULL,
|
||||
type_id int(10) DEFAULT NULL NULL,
|
||||
transaction_link_id int(10) unsigned DEFAULT NULL NULL,
|
||||
amount decimal(40,20) DEFAULT NULL NULL,
|
||||
balance decimal(40,20) DEFAULT NULL NULL,
|
||||
balance_date datetime(3) DEFAULT current_timestamp(3) NOT NULL,
|
||||
decay decimal(40,20) DEFAULT NULL NULL,
|
||||
decay_start datetime(3) DEFAULT NULL NULL,
|
||||
memo varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
creation_date datetime(3) DEFAULT NULL NULL,
|
||||
user_id int(10) unsigned NOT NULL,
|
||||
user_gradido_id char(36) NOT NULL,
|
||||
user_name varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL,
|
||||
user_community_uuid char(36) NOT NULL,
|
||||
linked_user_id int(10) unsigned DEFAULT NULL NULL,
|
||||
linked_user_gradido_id char(36) NOT NULL,
|
||||
linked_user_name varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL,
|
||||
linked_user_community_uuid char(36) NOT NULL,
|
||||
linked_transaction_id int(10) DEFAULT NULL NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE pending_transactions;`)
|
||||
}
|
||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0070-add_dlt_transactions_table',
|
||||
DB_VERSION: '0071-add-pending_transactions-table',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
|
||||
@ -49,8 +49,13 @@ When('the user submits the transaction by confirming', () => {
|
||||
cy.wrap(interception.request.body).should(
|
||||
'have.property',
|
||||
'query',
|
||||
`mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
|
||||
`mutation ($recipientCommunityIdentifier: String!, $recipientIdentifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(
|
||||
recipientCommunityIdentifier: $recipientCommunityIdentifier
|
||||
recipientIdentifier: $recipientIdentifier
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
)
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 72,
|
||||
lines: 76,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -11,7 +11,7 @@ Decimal.set({
|
||||
*/
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0070-add_dlt_transactions_table',
|
||||
DB_VERSION: '0071-add-pending_transactions-table',
|
||||
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class GetPublicCommunityInfoResult {
|
||||
constructor(dbCom: DbCommunity) {
|
||||
this.publicKey = dbCom.publicKey.toString()
|
||||
this.name = dbCom.name
|
||||
this.description = dbCom.description
|
||||
this.creationDate = dbCom.creationDate
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
name: string | null
|
||||
|
||||
@Field(() => String)
|
||||
description: string | null
|
||||
|
||||
@Field(() => Date)
|
||||
creationDate: Date | null
|
||||
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '@/server/createServer'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
let query: any
|
||||
|
||||
// to do: We need a setup for the tests that closes the connection
|
||||
let con: any
|
||||
|
||||
CONFIG.FEDERATION_API = '1_0'
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await createServer()
|
||||
con = server.con
|
||||
query = createTestClient(server.apollo).query
|
||||
DbCommunity.clear()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('PublicCommunityInfoResolver', () => {
|
||||
const getPublicCommunityInfoQuery = `
|
||||
query {
|
||||
getPublicCommunityInfo
|
||||
{
|
||||
name
|
||||
description
|
||||
creationDate
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('getPublicCommunityInfo', () => {
|
||||
let homeCom: DbCommunity
|
||||
beforeEach(async () => {
|
||||
homeCom = new DbCommunity()
|
||||
homeCom.foreign = false
|
||||
homeCom.url = 'homeCommunity-url'
|
||||
homeCom.name = 'Community-Name'
|
||||
homeCom.description = 'Community-Description'
|
||||
homeCom.creationDate = new Date()
|
||||
homeCom.publicKey = Buffer.from('homeCommunity-publicKey')
|
||||
await DbCommunity.insert(homeCom)
|
||||
})
|
||||
|
||||
it('returns public CommunityInfo', async () => {
|
||||
await expect(query({ query: getPublicCommunityInfoQuery })).resolves.toMatchObject({
|
||||
data: {
|
||||
getPublicCommunityInfo: {
|
||||
name: 'Community-Name',
|
||||
description: 'Community-Description',
|
||||
creationDate: homeCom.creationDate?.toISOString(),
|
||||
publicKey: expect.stringMatching('homeCommunity-publicKey'),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,18 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { GetPublicCommunityInfoResult } from '../model/GetPublicCommunityInfoResult'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class PublicCommunityInfoResolver {
|
||||
@Query(() => GetPublicCommunityInfoResult)
|
||||
async getPublicCommunityInfo(): Promise<GetPublicCommunityInfoResult> {
|
||||
logger.debug(`getPublicCommunityInfo() via apiVersion=1_0 ...`)
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
const result = new GetPublicCommunityInfoResult(homeCom)
|
||||
logger.info(`getPublicCommunityInfo()-1_0... return publicInfo=${JSON.stringify(result)}`)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||
"target": "esNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
|
||||
58
frontend/src/components/CommunitySwitch.vue
Normal file
58
frontend/src/components/CommunitySwitch.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="community-switch">
|
||||
<b-dropdown no-flip :text="value.name">
|
||||
<b-dropdown-item
|
||||
v-for="community in communities"
|
||||
@click.prevent="updateCommunity(community)"
|
||||
:key="community.id"
|
||||
:title="community.description"
|
||||
:active="value.uuid === community.uuid"
|
||||
>
|
||||
{{ community.name }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { selectCommunities } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'CommunitySwitch',
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
communities: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCommunity(community) {
|
||||
this.$emit('input', community)
|
||||
},
|
||||
setDefaultCommunity() {
|
||||
// set default community, the only one which isn't foreign
|
||||
// we assume it is only one entry with foreign = false
|
||||
if (this.value.uuid === '' && this.communities.length) {
|
||||
const foundCommunity = this.communities.find((community) => !community.foreign)
|
||||
if (foundCommunity) {
|
||||
this.updateCommunity(foundCommunity)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
communities: {
|
||||
query: selectCommunities,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaultCommunity()
|
||||
},
|
||||
updated() {
|
||||
this.setDefaultCommunity()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -6,7 +6,7 @@
|
||||
<b-col cols="12">
|
||||
<b-row class="mt-3">
|
||||
<b-col class="h5">{{ $t('form.recipientCommunity') }}</b-col>
|
||||
<b-col>{{ communityName }}</b-col>
|
||||
<b-col>{{ targetCommunity.name }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="h5">{{ $t('form.recipient') }}</b-col>
|
||||
@ -76,11 +76,16 @@ export default {
|
||||
amount: { type: Number, required: true },
|
||||
memo: { type: String, required: true },
|
||||
userName: { type: String, default: '' },
|
||||
targetCommunity: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return { uuid: '', name: COMMUNITY_NAME }
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disabled: false,
|
||||
communityName: COMMUNITY_NAME,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import flushPromises from 'flush-promises'
|
||||
import { SEND_TYPES } from '@/pages/Send'
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
import { user as userQuery, selectCommunities as selectCommunitiesQuery } from '@/graphql/queries'
|
||||
|
||||
const mockClient = createMockClient()
|
||||
const apolloProvider = new VueApollo({
|
||||
@ -61,6 +61,28 @@ describe('TransactionForm', () => {
|
||||
}),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
selectCommunitiesQuery,
|
||||
jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
communities: [
|
||||
{
|
||||
uuid: '8f4c146a-79b5-413f-89ed-53f624ec49b2',
|
||||
name: 'Gradido Entwicklung',
|
||||
description: 'Gradido-Community einer lokalen Entwicklungsumgebung.',
|
||||
foreign: false,
|
||||
},
|
||||
{
|
||||
uuid: 'ashasas',
|
||||
name: 'Hunde-Community',
|
||||
description: 'Hier geht es um Hunde',
|
||||
foreign: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
@ -352,6 +374,12 @@ Die ganze Welt bezwingen.“`)
|
||||
memo: 'Long enough',
|
||||
selected: 'send',
|
||||
userName: '',
|
||||
targetCommunity: {
|
||||
description: 'Gradido-Community einer lokalen Entwicklungsumgebung.',
|
||||
foreign: false,
|
||||
name: 'Gradido Entwicklung',
|
||||
uuid: '8f4c146a-79b5-413f-89ed-53f624ec49b2',
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
@ -54,7 +54,12 @@
|
||||
<b-col>{{ $t('form.recipientCommunity') }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="font-weight-bold">{{ communityName }}</b-col>
|
||||
<b-col class="font-weight-bold">
|
||||
<community-switch
|
||||
v-model="form.targetCommunity"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col cols="12" v-if="radioSelected === sendTypes.send">
|
||||
@ -137,6 +142,7 @@ import { SEND_TYPES } from '@/pages/Send'
|
||||
import InputIdentifier from '@/components/Inputs/InputIdentifier'
|
||||
import InputAmount from '@/components/Inputs/InputAmount'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea'
|
||||
import CommunitySwitch from '@/components/CommunitySwitch.vue'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { COMMUNITY_NAME } from '@/config'
|
||||
@ -147,6 +153,7 @@ export default {
|
||||
InputIdentifier,
|
||||
InputAmount,
|
||||
InputTextarea,
|
||||
CommunitySwitch,
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
@ -154,6 +161,12 @@ export default {
|
||||
amount: { type: Number, default: 0 },
|
||||
memo: { type: String, default: '' },
|
||||
selected: { type: String, default: 'send' },
|
||||
targetCommunity: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return { uuid: '', name: COMMUNITY_NAME }
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -161,10 +174,10 @@ export default {
|
||||
identifier: this.identifier,
|
||||
amount: this.amount ? String(this.amount) : '',
|
||||
memo: this.memo,
|
||||
targetCommunity: this.targetCommunity,
|
||||
},
|
||||
radioSelected: this.selected,
|
||||
userName: '',
|
||||
communityName: COMMUNITY_NAME,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -179,6 +192,7 @@ export default {
|
||||
amount: Number(this.form.amount.replace(',', '.')),
|
||||
memo: this.form.memo,
|
||||
userName: this.userName,
|
||||
targetCommunity: this.form.targetCommunity,
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
@ -186,6 +200,7 @@ export default {
|
||||
this.form.identifier = ''
|
||||
this.form.amount = ''
|
||||
this.form.memo = ''
|
||||
this.form.targetCommunity = { uuid: '', name: COMMUNITY_NAME }
|
||||
this.$refs.formValidator.validate()
|
||||
if (this.$route.query && !isEmpty(this.$route.query))
|
||||
this.$router.replace({ query: undefined })
|
||||
|
||||
@ -71,12 +71,17 @@ export const createUser = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation ($identifier: String!, $amount: Decimal!, $memo: String!, $communityIdentifier: String) {
|
||||
mutation(
|
||||
$recipientCommunityIdentifier: String!
|
||||
$recipientIdentifier: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
) {
|
||||
sendCoins(
|
||||
identifier: $identifier
|
||||
recipientCommunityIdentifier: $recipientCommunityIdentifier
|
||||
recipientIdentifier: $recipientIdentifier
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
communityIdentifier: $communityIdentifier
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
@ -72,14 +72,13 @@ export const listGDTEntriesQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const communities = gql`
|
||||
export const selectCommunities = gql`
|
||||
query {
|
||||
communities {
|
||||
id
|
||||
uuid
|
||||
name
|
||||
url
|
||||
description
|
||||
registerUrl
|
||||
foreign
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Send, { SEND_TYPES } from './Send'
|
||||
import Send from './Send'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
import { TRANSACTION_STEPS } from '@/components/GddSend'
|
||||
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
|
||||
@ -118,11 +118,10 @@ describe('Send', () => {
|
||||
expect.objectContaining({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'user@example.org',
|
||||
recipientIdentifier: 'user@example.org',
|
||||
amount: 23.45,
|
||||
memo: 'Make the best of it!',
|
||||
selected: SEND_TYPES.send,
|
||||
userName: '',
|
||||
recipientCommunityIdentifier: '',
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ -217,11 +216,10 @@ describe('Send', () => {
|
||||
expect.objectContaining({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'gradido-ID',
|
||||
recipientIdentifier: 'gradido-ID',
|
||||
amount: 34.56,
|
||||
memo: 'Make the best of it!',
|
||||
selected: SEND_TYPES.send,
|
||||
userName: '',
|
||||
recipientCommunityIdentifier: '',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -122,7 +122,13 @@ export default {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: sendCoins,
|
||||
variables: this.transactionData,
|
||||
variables: {
|
||||
// from target community we need only the uuid
|
||||
recipientCommunityIdentifier: this.transactionData.targetCommunity.uuid,
|
||||
recipientIdentifier: this.transactionData.identifier,
|
||||
amount: this.transactionData.amount,
|
||||
memo: this.transactionData.memo,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.error = false
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user