Merge branch 'master' into event_refactor_database

This commit is contained in:
Ulf Gebhardt 2023-02-17 14:48:16 +01:00 committed by GitHub
commit 53f684d865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 455 additions and 539 deletions

View File

@ -163,7 +163,6 @@ jobs:
locales_frontend:
name: Locales - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -171,20 +170,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
run: cd frontend && yarn && yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
@ -192,7 +181,6 @@ jobs:
lint_frontend:
name: Lint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -200,20 +188,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
run: cd frontend && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
@ -221,7 +199,6 @@ jobs:
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -229,20 +206,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint
run: cd frontend && yarn && yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
@ -250,7 +217,6 @@ jobs:
lint_admin:
name: Lint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -258,20 +224,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LINT ADMIN INTERFACE ###################################################
##########################################################################
- name: Admin Interface | Lint
run: docker run --rm gradido/admin:test yarn run lint
run: cd admin && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT ADMIN INTERFACE #############################################
@ -279,7 +235,6 @@ jobs:
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -287,20 +242,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint
run: cd admin && yarn && yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
@ -308,7 +253,6 @@ jobs:
locales_admin:
name: Locales - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -316,20 +260,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: admin | Locales
run: docker run --rm gradido/admin:test yarn run locales
- name: Admin | Locales
run: cd admin && yarn && yarn run locales
##############################################################################
# JOB: LINT BACKEND ##########################################################
@ -337,7 +271,6 @@ jobs:
lint_backend:
name: Lint - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -345,20 +278,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint
run: cd backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
@ -366,7 +289,6 @@ jobs:
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -385,7 +307,6 @@ jobs:
lint_database_up:
name: Lint - Database Up
runs-on: ubuntu-latest
needs: [build_test_database_up]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -393,20 +314,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
##########################################################################
# LINT DATABASE ##########################################################
##########################################################################
- name: database | Lint
run: docker run --rm gradido/database:test_up yarn run lint
- name: Database | Lint
run: cd database && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST FRONTEND ###################################################
@ -414,7 +325,6 @@ jobs:
unit_test_frontend:
name: Unit tests - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -422,30 +332,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# UNIT TESTS FRONTEND ####################################################
##########################################################################
- name: frontend | Unit tests
- name: Frontend | Unit tests
run: |
docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test
cp -r ~/coverage ./coverage
##########################################################################
# COVERAGE REPORT FRONTEND ###############################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
cd frontend && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK FRONTEND ################################################
##########################################################################
@ -454,7 +346,7 @@ jobs:
with:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
result_path: ./frontend/coverage/lcov.info
min_coverage: 95
token: ${{ github.token }}
@ -464,7 +356,6 @@ jobs:
unit_test_admin:
name: Unit tests - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -472,22 +363,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# UNIT TESTS ADMIN INTERFACE #############################################
##########################################################################
- name: Admin Interface | Unit tests
run: |
docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test
cp -r ~/coverage ./coverage
cd admin && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK ADMIN INTERFACE #########################################
##########################################################################
@ -496,7 +377,7 @@ jobs:
with:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
result_path: ./admin/coverage/lcov.info
min_coverage: 97
token: ${{ github.token }}
@ -534,8 +415,9 @@ jobs:
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
run: |
cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################

View File

@ -1,9 +1,10 @@
import jwt from 'jsonwebtoken'
import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload'
import LogError from '@/server/LogError'
export const decode = (token: string): CustomJwtPayload | null => {
if (!token) throw new Error('401 Unauthorized')
if (!token) throw new LogError('401 Unauthorized')
try {
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
} catch (err) {

View File

@ -7,6 +7,7 @@ import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { User } from '@entity/User'
import LogError from '@/server/LogError'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
@ -17,13 +18,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
// Do we have a token?
if (!context.token) {
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
throw new LogError('403.13 - Client certificate revoked')
}
// Set context gradidoID
context.gradidoID = decoded.gradidoID
@ -39,13 +40,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// check for correct rights
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
if (missingRights.length !== 0) {
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// set new header token

View File

@ -257,17 +257,13 @@ describe('Contribution Links', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError('Start-Date is not initialized. A Start-Date must be set!'),
],
errors: [new GraphQLError('A Start-Date must be set')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Start-Date is not initialized. A Start-Date must be set!',
)
expect(logger.error).toBeCalledWith('A Start-Date must be set')
})
it('returns an error if missing endDate', async () => {
@ -282,15 +278,13 @@ describe('Contribution Links', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('End-Date is not initialized. An End-Date must be set!')],
errors: [new GraphQLError('An End-Date must be set')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'End-Date is not initialized. An End-Date must be set!',
)
expect(logger.error).toBeCalledWith('An End-Date must be set')
})
it('returns an error if endDate is before startDate', async () => {
@ -307,7 +301,7 @@ describe('Contribution Links', () => {
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(`The value of validFrom must before or equals the validTo!`),
new GraphQLError(`The value of validFrom must before or equals the validTo`),
],
}),
)
@ -315,7 +309,7 @@ describe('Contribution Links', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
`The value of validFrom must before or equals the validTo!`,
`The value of validFrom must before or equals the validTo`,
)
})

View File

@ -33,10 +33,14 @@ export class ContributionMessageResolver {
try {
const contribution = await DbContribution.findOne({ id: contributionId })
if (!contribution) {
throw new Error('Contribution not found')
throw new LogError('Contribution not found', contributionId)
}
if (contribution.userId !== user.id) {
throw new Error('Can not send message to contribution of another user')
throw new LogError(
'Can not send message to contribution of another user',
contribution.userId,
user.id,
)
}
contributionMessage.contributionId = contributionId

View File

@ -245,8 +245,8 @@ describe('ContributionResolver', () => {
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
'No information for available creations for the given date',
expect.any(Date),
)
})
@ -268,8 +268,8 @@ describe('ContributionResolver', () => {
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
'No information for available creations for the given date',
expect.any(Date),
)
})
})
@ -526,14 +526,16 @@ describe('ContributionResolver', () => {
})
expect(errorObjects).toEqual([
new GraphQLError(
'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
),
])
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
new Decimal(1019),
new Decimal(600),
)
})
})
@ -2008,8 +2010,8 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
new Date(variables.creationDate).toString(),
'No information for available creations for the given date',
new Date(variables.creationDate),
)
})
})
@ -2033,8 +2035,8 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
new Date(variables.creationDate).toString(),
'No information for available creations for the given date',
new Date(variables.creationDate),
)
})
})
@ -2049,7 +2051,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
),
],
}),
@ -2058,7 +2060,9 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
new Decimal(2000),
new Decimal(790),
)
})
})
@ -2098,7 +2102,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
),
],
}),
@ -2107,7 +2111,9 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
new Decimal(1000),
new Decimal(590),
)
})
})
@ -2300,7 +2306,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
),
],
}),
@ -2309,7 +2315,9 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
new Decimal(1900),
new Decimal(1000),
)
})
})

View File

@ -8,6 +8,7 @@ import { Context, getUser } from '@/server/context'
import CONFIG from '@/config'
import { apiGet, apiPost } from '@/apis/HttpRequest'
import { RIGHTS } from '@/auth/RIGHTS'
import LogError from '@/server/LogError'
@Resolver()
export class GdtResolver {
@ -25,11 +26,11 @@ export class GdtResolver {
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
)
if (!resultGDT.success) {
throw new Error(resultGDT.data)
throw new LogError(resultGDT.data)
}
return new GdtEntryList(resultGDT.data)
} catch (err) {
throw new Error('GDT Server is not reachable.')
throw new LogError('GDT Server is not reachable')
}
}
@ -42,7 +43,7 @@ export class GdtResolver {
email: user.emailContact.email,
})
if (!resultGDTSum.success) {
throw new Error('Call not successful')
throw new LogError('Call not successful')
}
return Number(resultGDTSum.data.sum) || 0
} catch (err) {
@ -59,7 +60,7 @@ export class GdtResolver {
// load user
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
if (!resultPID.success) {
throw new Error(resultPID.data)
throw new LogError(resultPID.data)
}
return resultPID.data.pid
}

View File

@ -53,65 +53,81 @@ afterAll(async () => {
describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(
mutate({ mutation: createTransactionLink, variables: { amount: 0, memo: 'Test' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
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')],
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
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')],
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')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
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')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
@ -121,236 +137,37 @@ describe('TransactionLinkResolver', () => {
resetToken()
})
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
it('throws error when link does not exists', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-123456',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('No contribution link found to given code'),
)
})
const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('throws error when link is not valid yet', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is not valid yet'),
)
})
it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'INVALID',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(
mutate({ mutation: redeemTransactionLink, variables: { code: 'CL-123456' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => {
const now = new Date()
let contributionLink: DbContributionLink | undefined
let contribution: UnconfirmedContribution | undefined
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
})
it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
contributionLink = cls[0]
expect(contributionLink).toEqual(
expect.objectContaining({
id: expect.any(Number),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
validFrom: new Date(now.getFullYear(), 0, 1),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
cycle: 'DAILY',
maxPerCycle: 1,
totalMaxCountOfContribution: null,
maxAccountBalance: null,
minGapHours: null,
createdAt: expect.any(Date),
deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true,
amount: expect.decimalEqual(5),
maxAmountPerMonth: expect.decimalEqual(200),
}),
)
})
describe('user has pending contribution of 1000 GDD', () => {
describe('authenticated', () => {
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const result = await mutate({
mutation: createContribution,
variables: {
amount: new Decimal(1000),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
contribution = result.data.createContribution
})
it('does not allow the user to redeem the contribution link', async () => {
it('throws error when link does not exists', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
code: 'CL-123456',
},
}),
).resolves.toMatchObject({
@ -359,85 +176,247 @@ describe('TransactionLinkResolver', () => {
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
new Error('No contribution link found to given code'),
)
})
})
describe('user has no pending contributions that would not allow to redeem the link', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: updateContribution,
variables: {
contributionId: contribution ? contribution.id : -1,
amount: new Decimal(800),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
})
const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('allows the user to redeem the contribution link', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
data: {
redeemTransactionLink: true,
},
errors: undefined,
})
})
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
it('throws error when link is not valid yet', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
new Error('Contribution link is not valid yet'),
)
})
describe('after one day', () => {
it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'INVALID',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
})
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => {
const now = new Date()
let contributionLink: DbContributionLink | undefined
let contribution: UnconfirmedContribution | undefined
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
})
it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
contributionLink = cls[0]
expect(contributionLink).toEqual(
expect.objectContaining({
id: expect.any(Number),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
validFrom: new Date(now.getFullYear(), 0, 1),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
cycle: 'DAILY',
maxPerCycle: 1,
totalMaxCountOfContribution: null,
maxAccountBalance: null,
minGapHours: null,
createdAt: expect.any(Date),
deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true,
amount: expect.decimalEqual(5),
maxAmountPerMonth: expect.decimalEqual(200),
}),
)
})
describe('user has pending contribution of 1000 GDD', () => {
beforeAll(async () => {
jest.useFakeTimers()
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const result = await mutate({
mutation: createContribution,
variables: {
amount: new Decimal(1000),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
contribution = result.data.createContribution
})
afterAll(() => {
jest.useRealTimers()
it('does not allow the user to redeem the contribution link', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
})
it('allows the user to redeem the contribution link again', async () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount to be created exceeds the amount still available for this month',
),
)
})
})
describe('user has no pending contributions that would not allow to redeem the link', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: updateContribution,
variables: {
contributionId: contribution ? contribution.id : -1,
amount: new Decimal(800),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
})
it('allows the user to redeem the contribution link', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
@ -473,6 +452,59 @@ describe('TransactionLinkResolver', () => {
new Error('Contribution link already redeemed today'),
)
})
describe('after one day', () => {
beforeAll(async () => {
jest.useFakeTimers()
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
jest.useRealTimers()
})
it('allows the user to redeem the contribution link again', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
data: {
redeemTransactionLink: true,
},
errors: undefined,
})
})
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
})
})
})
})

View File

@ -86,8 +86,8 @@ export class TransactionLinkResolver {
transactionLink.code = transactionLinkCode(createdDate)
transactionLink.createdAt = createdDate
transactionLink.validUntil = validUntil
await DbTransactionLink.save(transactionLink).catch(() => {
throw new Error('Unable to save transaction link')
await DbTransactionLink.save(transactionLink).catch((e) => {
throw new LogError('Unable to save transaction link', e)
})
return new TransactionLink(transactionLink, new User(user))
@ -103,19 +103,23 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOne({ id })
if (!transactionLink) {
throw new Error('Transaction Link not found!')
throw new LogError('Transaction link not found', id)
}
if (transactionLink.userId !== user.id) {
throw new Error('Transaction Link cannot be deleted!')
throw new LogError(
'Transaction link cannot be deleted by another user',
transactionLink.userId,
user.id,
)
}
if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed!')
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await transactionLink.softRemove().catch(() => {
throw new Error('Transaction Link could not be deleted!')
await transactionLink.softRemove().catch((e) => {
throw new LogError('Transaction link could not be deleted', e)
})
return true
@ -312,18 +316,18 @@ export class TransactionLinkResolver {
)
if (user.id === linkedUser.id) {
throw new Error('Cannot redeem own transaction link.')
throw new LogError('Cannot redeem own transaction link', user.id)
}
// TODO: The now check should be done within the semaphore lock,
// since the program might wait a while till it is ready to proceed
// writing the transaction.
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.')
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
}
if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed.')
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await executeTransaction(

View File

@ -1,3 +1,4 @@
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { getConnection } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
@ -19,19 +20,14 @@ export const validateContribution = (
const index = getCreationIndex(creationDate.getMonth(), timezoneOffset)
if (index < 0) {
logger.error(
'No information for available creations with the given creationDate=',
creationDate.toString(),
)
throw new Error('No information for available creations for the given date')
throw new LogError('No information for available creations for the given date', creationDate)
}
if (amount.greaterThan(creations[index].toString())) {
logger.error(
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
)
throw new Error(
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
throw new LogError(
'The amount to be created exceeds the amount still available for this month',
amount,
creations[index],
)
}
}
@ -126,19 +122,16 @@ export const isStartEndDateValid = (
endDate: string | null | undefined,
): void => {
if (!startDate) {
logger.error('Start-Date is not initialized. A Start-Date must be set!')
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
throw new LogError('A Start-Date must be set')
}
if (!endDate) {
logger.error('End-Date is not initialized. An End-Date must be set!')
throw new Error('End-Date is not initialized. An End-Date must be set!')
throw new LogError('An End-Date must be set')
}
// check if endDate is before startDate
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
logger.error(`The value of validFrom must before or equals the validTo!`)
throw new Error(`The value of validFrom must before or equals the validTo!`)
throw new LogError(`The value of validFrom must before or equals the validTo`)
}
}
@ -150,7 +143,7 @@ export const updateCreations = (
const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset)
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
throw new LogError('You cannot create GDD for a month older than the last three months')
}
creations[index] = creations[index].plus(contribution.amount.toString())
return creations

View File

@ -1,4 +1,5 @@
import CONFIG from '@/config'
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { User } from '@entity/User'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
@ -16,11 +17,10 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
logger.error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
)
throw new Error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
throw new LogError(
'ServerKey has an invalid size',
configLoginServerKey.length,
sodium.crypto_shorthash_KEYBYTES,
)
}
@ -52,20 +52,13 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
export const getUserCryptographicSalt = (dbUser: User): string => {
switch (dbUser.passwordEncryptionType) {
case PasswordEncryptionType.NO_PASSWORD: {
logger.error('Password not set for user ' + dbUser.id)
throw new Error('Password not set for user ' + dbUser.id) // user has no password
}
case PasswordEncryptionType.EMAIL: {
case PasswordEncryptionType.NO_PASSWORD:
throw new LogError('User has no password set', dbUser.id)
case PasswordEncryptionType.EMAIL:
return dbUser.emailContact.email
break
}
case PasswordEncryptionType.GRADIDO_ID: {
case PasswordEncryptionType.GRADIDO_ID:
return dbUser.gradidoID
break
}
default:
logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType)
}
}

View File

@ -3,6 +3,7 @@ import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { ExpressContext } from 'apollo-server-express'
import LogError from './LogError'
export interface Context {
token: string | null
@ -35,7 +36,7 @@ const context = (args: ExpressContext): Context => {
export const getUser = (context: Context): dbUser => {
if (context.user) return context.user
throw new Error('No user given in context!')
throw new LogError('No user given in context')
}
export const getClientTimezoneOffset = (context: Context): number => {
@ -45,7 +46,7 @@ export const getClientTimezoneOffset = (context: Context): number => {
) {
return context.clientTimezoneOffset
}
throw new Error('No valid client time zone offset in context!')
throw new LogError('No valid client time zone offset in context')
}
export default context

View File

@ -1,6 +1,7 @@
import Decimal from 'decimal.js-light'
import CONFIG from '@/config'
import { Decay } from '@model/Decay'
import LogError from '@/server/LogError'
// TODO: externalize all those definitions and functions into an external decay library
@ -22,7 +23,7 @@ function calculateDecay(
const startBlockMs = startBlock.getTime()
if (toMs < fromMs) {
throw new Error('to < from, reverse decay calculation is invalid')
throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid')
}
// Initialize with no decay

View File

@ -1,11 +1,12 @@
import connection from '@/typeorm/connection'
import { getKlickTippUser } from '@/apis/KlicktippController'
import { User } from '@entity/User'
import LogError from '@/server/LogError'
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
const con = await connection()
if (!con) {
throw new Error('No connection to database')
throw new LogError('No connection to database')
}
const users = await User.find({ relations: ['emailContact'] })
const notRegisteredUser = []