Merge branch 'master' into 2224-feature-concept-extend-contributionlink-rules-and-logic-abraham

This commit is contained in:
clauspeterhuebner 2022-10-17 22:52:34 +02:00 committed by GitHub
commit 417e348a34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 804 additions and 424 deletions

View File

@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 68 min_coverage: 74
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -5,6 +5,7 @@ const localVue = global.localVue
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d),
} }
const propsData = { const propsData = {

View File

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ContributionLinkForm from './ContributionLinkForm.vue' import ContributionLinkForm from './ContributionLinkForm.vue'
import { toastErrorSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import { createContributionLink } from '@/graphql/createContributionLink.js'
const localVue = global.localVue const localVue = global.localVue
@ -72,48 +73,70 @@ describe('ContributionLinkForm', () => {
}) })
}) })
// describe('successfull submit', () => { describe('successfull submit', () => {
// beforeEach(async () => { beforeEach(async () => {
// mockAPIcall.mockResolvedValue({ apolloMutateMock.mockResolvedValue({
// data: { data: {
// createContributionLink: { createContributionLink: {
// link: 'https://localhost/redeem/CL-1a2345678', link: 'https://localhost/redeem/CL-1a2345678',
// }, },
// }, },
// }) })
// await wrapper.find('input.test-validFrom').setValue('2022-6-18') await wrapper
// await wrapper.find('input.test-validTo').setValue('2022-7-18') .findAllComponents({ name: 'BFormDatepicker' })
// await wrapper.find('input.test-name').setValue('test name') .at(0)
// await wrapper.find('input.test-memo').setValue('test memo') .vm.$emit('input', '2022-6-18')
// await wrapper.find('input.test-amount').setValue('100') await wrapper
// await wrapper.find('form').trigger('submit') .findAllComponents({ name: 'BFormDatepicker' })
// }) .at(1)
.vm.$emit('input', '2022-7-18')
await wrapper.find('input.test-name').setValue('test name')
await wrapper.find('textarea.test-memo').setValue('test memo')
await wrapper.find('input.test-amount').setValue('100')
await wrapper.find('form').trigger('submit')
})
// it('calls the API', () => { it('calls the API', () => {
// expect(mockAPIcall).toHaveBeenCalledWith( expect(apolloMutateMock).toHaveBeenCalledWith({
// expect.objectContaining({ mutation: createContributionLink,
// variables: { variables: {
// link: 'https://localhost/redeem/CL-1a2345678', validFrom: '2022-6-18',
// }, validTo: '2022-7-18',
// }), name: 'test name',
// ) amount: '100',
// }) memo: 'test memo',
cycle: 'ONCE',
maxPerCycle: 1,
maxAmountPerMonth: '0',
},
})
})
// it('displays the new username', () => { it('toasts a succes message', () => {
// expect(wrapper.find('div.display-username').text()).toEqual('@username') expect(toastSuccessSpy).toBeCalledWith('https://localhost/redeem/CL-1a2345678')
// }) })
// })
})
describe('send createContributionLink with error', () => {
beforeEach(() => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
wrapper.vm.onSubmit()
}) })
it('toasts an error message', () => { describe('send createContributionLink with error', () => {
expect(toastErrorSpy).toBeCalledWith('contributionLink.noStartDate') beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
await wrapper
.findAllComponents({ name: 'BFormDatepicker' })
.at(0)
.vm.$emit('input', '2022-6-18')
await wrapper
.findAllComponents({ name: 'BFormDatepicker' })
.at(1)
.vm.$emit('input', '2022-7-18')
await wrapper.find('input.test-name').setValue('test name')
await wrapper.find('textarea.test-memo').setValue('test memo')
await wrapper.find('input.test-amount').setValue('100')
await wrapper.find('form').trigger('submit')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
}) })
}) })
}) })

View File

@ -1,8 +1,5 @@
<template> <template>
<div class="contribution-link-form"> <div class="contribution-link-form">
<div v-if="updateData" class="text-light bg-info p-3">
{{ updateData }}
</div>
<b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm"> <b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm">
<!-- Date --> <!-- Date -->
<b-row> <b-row>
@ -68,34 +65,32 @@
class="test-amount" class="test-amount"
></b-form-input> ></b-form-input>
</b-form-group> </b-form-group>
<b-collapse id="collapse-2"> <b-row class="mb-4">
<b-jumbotron> <b-col>
<b-row class="mb-4"> <!-- Cycle -->
<b-col> <label for="cycle">{{ $t('contributionLink.cycle') }}</label>
<!-- Cycle --> <b-form-select
<label for="cycle">{{ $t('contributionLink.cycle') }}</label> v-model="form.cycle"
<b-form-select :options="cycle"
v-model="form.cycle" class="mb-3"
:options="cycle" size="lg"
:disabled="disabled" ></b-form-select>
class="mb-3" </b-col>
size="lg" <b-col>
></b-form-select> <!-- maxPerCycle -->
</b-col> <label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
<b-col> <b-form-select
<!-- maxPerCycle --> v-model="form.maxPerCycle"
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label> :options="maxPerCycle"
<b-form-select :disabled="disabled"
v-model="form.maxPerCycle" class="mb-3"
:options="maxPerCycle" size="lg"
:disabled="disabled" ></b-form-select>
class="mb-3" </b-col>
size="lg" </b-row>
></b-form-select>
</b-col>
</b-row>
<!-- Max amount --> <!-- Max amount -->
<!--
<b-form-group :label="$t('contributionLink.maximumAmount')"> <b-form-group :label="$t('contributionLink.maximumAmount')">
<b-form-input <b-form-input
v-model="form.maxAmountPerMonth" v-model="form.maxAmountPerMonth"
@ -105,8 +100,7 @@
placeholder="0" placeholder="0"
></b-form-input> ></b-form-input>
</b-form-group> </b-form-group>
</b-jumbotron> -->
</b-collapse>
<div class="mt-6"> <div class="mt-6">
<b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button> <b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button>
<b-button type="reset" variant="danger" @click.prevent="onReset"> <b-button type="reset" variant="danger" @click.prevent="onReset">
@ -143,18 +137,18 @@ export default {
min: new Date(), min: new Date(),
cycle: [ cycle: [
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') }, { value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
{ value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') }, // { value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
{ value: 'daily', text: this.$t('contributionLink.options.cycle.daily') }, { value: 'DAILY', text: this.$t('contributionLink.options.cycle.daily') },
{ value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') }, // { value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
{ value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') }, // { value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
{ value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') }, // { value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
], ],
maxPerCycle: [ maxPerCycle: [
{ value: '1', text: '1 x' }, { value: '1', text: '1 x' },
{ value: '2', text: '2 x' }, // { value: '2', text: '2 x' },
{ value: '3', text: '3 x' }, // { value: '3', text: '3 x' },
{ value: '4', text: '4 x' }, // { value: '4', text: '4 x' },
{ value: '5', text: '5 x' }, // { value: '5', text: '5 x' },
], ],
} }
}, },
@ -195,12 +189,8 @@ export default {
}, },
}, },
computed: { computed: {
updateData() {
return this.contributionLinkData
},
disabled() { disabled() {
if (this.form.cycle === 'ONCE') return true return true
return false
}, },
}, },
watch: { watch: {

View File

@ -9,6 +9,7 @@ const mockAPIcall = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,
}, },

View File

@ -64,8 +64,28 @@ export default {
'amount', 'amount',
{ key: 'cycle', label: this.$t('contributionLink.cycle') }, { key: 'cycle', label: this.$t('contributionLink.cycle') },
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') }, { key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
{ key: 'validFrom', label: this.$t('contributionLink.validFrom') }, {
{ key: 'validTo', label: this.$t('contributionLink.validTo') }, key: 'validFrom',
label: this.$t('contributionLink.validFrom'),
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
} else {
return null
}
},
},
{
key: 'validTo',
label: this.$t('contributionLink.validTo'),
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
} else {
return null
}
},
},
'delete', 'delete',
'edit', 'edit',
'show', 'show',

View File

@ -12,6 +12,7 @@
value-field="item" value-field="item"
text-field="name" text-field="name"
name="month-selection" name="month-selection"
:disabled="true"
></b-form-radio-group> ></b-form-radio-group>
</b-row> </b-row>
<div class="m-4"> <div class="m-4">

View File

@ -3,11 +3,15 @@ import NavBar from './NavBar.vue'
const localVue = global.localVue const localVue = global.localVue
const apolloMutateMock = jest.fn()
const storeDispatchMock = jest.fn() const storeDispatchMock = jest.fn()
const routerPushMock = jest.fn() const routerPushMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$store: { $store: {
state: { state: {
openCreations: 1, openCreations: 1,
@ -69,5 +73,9 @@ describe('NavBar', () => {
it('dispatches logout to store', () => { it('dispatches logout to store', () => {
expect(storeDispatchMock).toBeCalledWith('logout') expect(storeDispatchMock).toBeCalledWith('logout')
}) })
it('has called logout mutation', () => {
expect(apolloMutateMock).toBeCalled()
})
}) })
}) })

View File

@ -28,14 +28,18 @@
</template> </template>
<script> <script>
import CONFIG from '../config' import CONFIG from '../config'
import { logout } from '../graphql/logout'
export default { export default {
name: 'navbar', name: 'navbar',
methods: { methods: {
logout() { async logout() {
window.location.assign(CONFIG.WALLET_URL) window.location.assign(CONFIG.WALLET_URL)
// window.location = CONFIG.WALLET_URL // window.location = CONFIG.WALLET_URL
this.$store.dispatch('logout') this.$store.dispatch('logout')
await this.$apollo.mutate({
mutation: logout,
})
}, },
wallet() { wallet() {
window.location = CONFIG.WALLET_AUTH_URL.replace('{token}', this.$store.state.token) window.location = CONFIG.WALLET_AUTH_URL.replace('{token}', this.$store.state.token)

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const logout = gql`
mutation {
logout
}
`

View File

@ -9,7 +9,6 @@
"cycle": "Zyklus", "cycle": "Zyklus",
"deleted": "Automatische Schöpfung gelöscht!", "deleted": "Automatische Schöpfung gelöscht!",
"deleteNow": "Automatische Creations '{name}' wirklich löschen?", "deleteNow": "Automatische Creations '{name}' wirklich löschen?",
"maximumAmount": "maximaler Betrag",
"maxPerCycle": "Wiederholungen", "maxPerCycle": "Wiederholungen",
"memo": "Nachricht", "memo": "Nachricht",
"name": "Name", "name": "Name",
@ -21,11 +20,7 @@
"options": { "options": {
"cycle": { "cycle": {
"daily": "täglich", "daily": "täglich",
"hourly": "stündlich", "once": "einmalig"
"monthly": "monatlich",
"once": "einmalig",
"weekly": "wöchentlich",
"yearly": "jährlich"
} }
}, },
"validFrom": "Startdatum", "validFrom": "Startdatum",

View File

@ -9,7 +9,6 @@
"cycle": "Cycle", "cycle": "Cycle",
"deleted": "Automatic creation deleted!", "deleted": "Automatic creation deleted!",
"deleteNow": "Do you really delete automatic creations '{name}'?", "deleteNow": "Do you really delete automatic creations '{name}'?",
"maximumAmount": "Maximum amount",
"maxPerCycle": "Repetition", "maxPerCycle": "Repetition",
"memo": "Memo", "memo": "Memo",
"name": "Name", "name": "Name",
@ -21,11 +20,7 @@
"options": { "options": {
"cycle": { "cycle": {
"daily": "daily", "daily": "daily",
"hourly": "hourly", "once": "once"
"monthly": "monthly",
"once": "once",
"weekly": "weekly",
"yearly": "yearly"
} }
}, },
"validFrom": "Start-date", "validFrom": "Start-date",

View File

@ -78,6 +78,7 @@ const storeCommitMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$d: jest.fn((d) => d),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },

View File

@ -5,6 +5,7 @@ module.exports = {
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'], modulePathIgnorePatterns: ['<rootDir>/build/'],
moduleNameMapper: { moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1', '@/(.*)': '<rootDir>/src/$1',

View File

@ -1,13 +1,14 @@
import { registerEnumType } from 'type-graphql' import { registerEnumType } from 'type-graphql'
// lowercase values are not implemented yet
export enum ContributionCycleType { export enum ContributionCycleType {
ONCE = 'once', ONCE = 'ONCE',
HOUR = 'hour', HOUR = 'hour',
TWO_HOURS = 'two_hours', TWO_HOURS = 'two_hours',
FOUR_HOURS = 'four_hours', FOUR_HOURS = 'four_hours',
EIGHT_HOURS = 'eight_hours', EIGHT_HOURS = 'eight_hours',
HALF_DAY = 'half_day', HALF_DAY = 'half_day',
DAY = 'day', DAILY = 'DAILY',
TWO_DAYS = 'two_days', TWO_DAYS = 'two_days',
THREE_DAYS = 'three_days', THREE_DAYS = 'three_days',
FOUR_DAYS = 'four_days', FOUR_DAYS = 'four_days',

View File

@ -13,6 +13,7 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { import {
login,
setUserRole, setUserRole,
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
@ -27,7 +28,6 @@ import {
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { import {
listUnconfirmedContributions, listUnconfirmedContributions,
login,
searchUsers, searchUsers,
listTransactionLinksAdmin, listTransactionLinksAdmin,
listContributionLinks, listContributionLinks,
@ -96,8 +96,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -121,8 +121,8 @@ describe('AdminResolver', () => {
describe('with admin rights', () => { describe('with admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig) admin = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -249,8 +249,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -274,8 +274,8 @@ describe('AdminResolver', () => {
describe('with admin rights', () => { describe('with admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig) admin = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -357,8 +357,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -382,8 +382,8 @@ describe('AdminResolver', () => {
describe('with admin rights', () => { describe('with admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig) admin = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -469,8 +469,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -514,8 +514,8 @@ describe('AdminResolver', () => {
beforeAll(async () => { beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig) admin = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
@ -766,8 +766,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -875,8 +875,8 @@ describe('AdminResolver', () => {
describe('with admin rights', () => { describe('with admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig) admin = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -1202,7 +1202,8 @@ describe('AdminResolver', () => {
}) })
describe('creation update is not valid', () => { describe('creation update is not valid', () => {
it('throws an error', async () => { // as this test has not clearly defined that date, it is a false positive
it.skip('throws an error', async () => {
await expect( await expect(
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
@ -1227,7 +1228,8 @@ describe('AdminResolver', () => {
}) })
describe('creation update is successful changing month', () => { describe('creation update is successful changing month', () => {
it('returns update creation object', async () => { // skipped as changing the month is currently disable
it.skip('returns update creation object', async () => {
await expect( await expect(
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
@ -1255,7 +1257,8 @@ describe('AdminResolver', () => {
}) })
describe('creation update is successful without changing month', () => { describe('creation update is successful without changing month', () => {
it('returns update creation object', async () => { // actually this mutation IS changing the month
it.skip('returns update creation object', async () => {
await expect( await expect(
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
@ -1299,10 +1302,10 @@ describe('AdminResolver', () => {
lastName: 'Lustig', lastName: 'Lustig',
email: 'peter@lustig.de', email: 'peter@lustig.de',
date: expect.any(String), date: expect.any(String),
memo: 'Das war leider zu Viel!', memo: 'Herzlich Willkommen bei Gradido!',
amount: '200', amount: '400',
moderator: admin.id, moderator: admin.id,
creation: ['1000', '1000', '300'], creation: ['1000', '600', '500'],
}, },
{ {
id: expect.any(Number), id: expect.any(Number),
@ -1313,7 +1316,7 @@ describe('AdminResolver', () => {
memo: 'Grundeinkommen', memo: 'Grundeinkommen',
amount: '500', amount: '500',
moderator: admin.id, moderator: admin.id,
creation: ['1000', '1000', '300'], creation: ['1000', '600', '500'],
}, },
{ {
id: expect.any(Number), id: expect.any(Number),
@ -1556,8 +1559,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -1602,8 +1605,8 @@ describe('AdminResolver', () => {
} }
// admin: only now log in // admin: only now log in
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -1792,13 +1795,14 @@ describe('AdminResolver', () => {
}) })
describe('Contribution Links', () => { describe('Contribution Links', () => {
const now = new Date()
const variables = { const variables = {
amount: new Decimal(200), amount: new Decimal(200),
name: 'Dokumenta 2022', name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022', memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
cycle: 'once', cycle: 'once',
validFrom: new Date(2022, 5, 18).toISOString(), validFrom: new Date(2022, 5, 18).toISOString(),
validTo: new Date(2022, 7, 14).toISOString(), validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
} }
@ -1862,8 +1866,8 @@ describe('AdminResolver', () => {
describe('without admin rights', () => { describe('without admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg) user = await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -1936,8 +1940,8 @@ describe('AdminResolver', () => {
describe('with admin rights', () => { describe('with admin rights', () => {
beforeAll(async () => { beforeAll(async () => {
user = await userFactory(testEnv, peterLustig) user = await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -1980,7 +1984,7 @@ describe('AdminResolver', () => {
name: 'Dokumenta 2022', name: 'Dokumenta 2022',
memo: 'Danke für deine Teilnahme an der Dokumenta 2022', memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
validFrom: new Date('2022-06-18T00:00:00.000Z'), validFrom: new Date('2022-06-18T00:00:00.000Z'),
validTo: new Date('2022-08-14T00:00:00.000Z'), validTo: expect.any(Date),
cycle: 'once', cycle: 'once',
maxPerCycle: 1, maxPerCycle: 1,
totalMaxCountOfContribution: null, totalMaxCountOfContribution: null,
@ -1990,8 +1994,8 @@ describe('AdminResolver', () => {
deletedAt: null, deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/), code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true, linkEnabled: true,
// amount: '200', amount: expect.decimalEqual(200),
// maxAmountPerMonth: '200', maxAmountPerMonth: expect.decimalEqual(200),
}), }),
) )
}) })
@ -2280,7 +2284,7 @@ describe('AdminResolver', () => {
id: linkId, id: linkId,
name: 'Dokumenta 2023', name: 'Dokumenta 2023',
memo: 'Danke für deine Teilnahme an der Dokumenta 2023', memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
// amount: '400', amount: expect.decimalEqual(400),
}), }),
) )
}) })

View File

@ -339,6 +339,9 @@ export class AdminResolver {
let creations = await getUserCreation(user.id) let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate) creations = updateCreations(creations, contributionToUpdate)
} else {
logger.error('Currently the month of the contribution cannot change.')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function
@ -675,6 +678,7 @@ export class AdminResolver {
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionLinkList> { ): Promise<ContributionLinkList> {
const [links, count] = await DbContributionLink.findAndCount({ const [links, count] = await DbContributionLink.findAndCount({
where: [{ validTo: MoreThan(new Date()) }, { validTo: IsNull() }],
order: { createdAt: order }, order: { createdAt: order },
skip: (currentPage - 1) * pageSize, skip: (currentPage - 1) * pageSize,
take: pageSize, take: pageSize,

View File

@ -7,8 +7,9 @@ import {
adminCreateContributionMessage, adminCreateContributionMessage,
createContribution, createContribution,
createContributionMessage, createContributionMessage,
login,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listContributionMessages, login } from '@/seeds/graphql/queries' import { listContributionMessages } from '@/seeds/graphql/queries'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
@ -21,14 +22,13 @@ jest.mock('@/mailer/sendAddedContributionMessageEmail', () => {
} }
}) })
let mutate: any, query: any, con: any let mutate: any, con: any
let testEnv: any let testEnv: any
let result: any let result: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con con = testEnv.con
await cleanDB() await cleanDB()
}) })
@ -59,8 +59,8 @@ describe('ContributionMessageResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
result = await mutate({ result = await mutate({
@ -71,8 +71,8 @@ describe('ContributionMessageResolver', () => {
creationDate: new Date().toString(), creationDate: new Date().toString(),
}, },
}) })
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -103,8 +103,8 @@ describe('ContributionMessageResolver', () => {
}) })
it('throws error when contribution.userId equals user.id', async () => { it('throws error when contribution.userId equals user.id', async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
const result2 = await mutate({ const result2 = await mutate({
@ -195,8 +195,8 @@ describe('ContributionMessageResolver', () => {
describe('authenticated', () => { describe('authenticated', () => {
beforeAll(async () => { beforeAll(async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -227,8 +227,8 @@ describe('ContributionMessageResolver', () => {
}) })
it('throws error when other user tries to send createContributionMessage', async () => { it('throws error when other user tries to send createContributionMessage', async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
await expect( await expect(
@ -253,8 +253,8 @@ describe('ContributionMessageResolver', () => {
describe('valid input', () => { describe('valid input', () => {
beforeAll(async () => { beforeAll(async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -304,8 +304,8 @@ describe('ContributionMessageResolver', () => {
describe('authenticated', () => { describe('authenticated', () => {
beforeAll(async () => { beforeAll(async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })

View File

@ -8,8 +8,9 @@ import {
createContribution, createContribution,
deleteContribution, deleteContribution,
updateContribution, updateContribution,
login,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries' import { listAllContributions, listContributions } from '@/seeds/graphql/queries'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
@ -54,8 +55,8 @@ describe('ContributionResolver', () => {
describe('authenticated with valid user', () => { describe('authenticated with valid user', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -197,8 +198,8 @@ describe('ContributionResolver', () => {
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!) await creationFactory(testEnv, bibisCreation!)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
await mutate({ await mutate({
@ -310,8 +311,8 @@ describe('ContributionResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
result = await mutate({ result = await mutate({
@ -393,8 +394,8 @@ describe('ContributionResolver', () => {
describe('wrong user tries to update the contribution', () => { describe('wrong user tries to update the contribution', () => {
beforeAll(async () => { beforeAll(async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
}) })
@ -445,8 +446,8 @@ describe('ContributionResolver', () => {
describe('update too much so that the limit is exceeded', () => { describe('update too much so that the limit is exceeded', () => {
beforeAll(async () => { beforeAll(async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
}) })
@ -489,9 +490,7 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [new GraphQLError('Currently the month of the contribution cannot change.')],
new GraphQLError('No information for available creations for the given date'),
],
}), }),
) )
}) })
@ -553,8 +552,8 @@ describe('ContributionResolver', () => {
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!) await creationFactory(testEnv, bibisCreation!)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
await mutate({ await mutate({
@ -630,8 +629,8 @@ describe('ContributionResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
result = await mutate({ result = await mutate({
@ -668,8 +667,8 @@ describe('ContributionResolver', () => {
describe('other user sends a deleteContribtuion', () => { describe('other user sends a deleteContribtuion', () => {
it('returns an error', async () => { it('returns an error', async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
await expect( await expect(
@ -702,8 +701,8 @@ describe('ContributionResolver', () => {
describe('User deletes already confirmed contribution', () => { describe('User deletes already confirmed contribution', () => {
it('throws an error', async () => { it('throws an error', async () => {
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
await mutate({ await mutate({
@ -712,8 +711,8 @@ describe('ContributionResolver', () => {
id: result.data.createContribution.id, id: result.data.createContribution.id,
}, },
}) })
await query({ await mutate({
query: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
await expect( await expect(

View File

@ -164,6 +164,9 @@ export class ContributionResolver {
let creations = await getUserCreation(user.id) let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate) creations = updateCreations(creations, contributionToUpdate)
} else {
logger.error('Currently the month of the contribution cannot change.')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function

View File

@ -1,4 +1,168 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { transactionLinkCode } from './TransactionLinkResolver' import { transactionLinkCode } from './TransactionLinkResolver'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { cleanDB, testEnvironment } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user'
import { login, createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql'
let mutate: any, con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('TransactionLinkResolver', () => {
describe('redeem daily Contribution Link', () => {
const now = new Date()
let contributionLink: DbContributionLink | 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),
}),
)
})
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 () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
})
})
describe('after one day', () => {
beforeAll(async () => {
jest.useFakeTimers()
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
setTimeout(() => {}, 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,
variables: { email: 'peter@lustig.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 () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
})
})
})
})
})
describe('transactionLinkCode', () => { describe('transactionLinkCode', () => {
const date = new Date() const date = new Date()

View File

@ -34,6 +34,7 @@ import { getUserCreation, validateContribution } from './util/creations'
import { Decay } from '@model/Decay' import { Decay } from '@model/Decay'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { TransactionTypeId } from '@enum/TransactionTypeId' import { TransactionTypeId } from '@enum/TransactionTypeId'
import { ContributionCycleType } from '@enum/ContributionCycleType'
const QueryLinkResult = createUnionType({ const QueryLinkResult = createUnionType({
name: 'QueryLinkResult', // the name of the GraphQL union name: 'QueryLinkResult', // the name of the GraphQL union
@ -204,23 +205,60 @@ export class TransactionLinkResolver {
throw new Error('Contribution link is depricated') throw new Error('Contribution link is depricated')
} }
} }
if (contributionLink.cycle !== 'ONCE') { let alreadyRedeemed: DbContribution | undefined
logger.error('contribution link has unknown cycle', contributionLink.cycle) switch (contributionLink.cycle) {
throw new Error('Contribution link has unknown cycle') case ContributionCycleType.ONCE: {
} alreadyRedeemed = await queryRunner.manager
// Test ONCE rule .createQueryBuilder()
const alreadyRedeemed = await queryRunner.manager .select('contribution')
.createQueryBuilder() .from(DbContribution, 'contribution')
.select('contribution') .where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
.from(DbContribution, 'contribution') linkId: contributionLink.id,
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', { id: user.id,
linkId: contributionLink.id, })
id: user.id, .getOne()
}) if (alreadyRedeemed) {
.getOne() logger.error(
if (alreadyRedeemed) { 'contribution link with rule ONCE already redeemed by user with id',
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id) user.id,
throw new Error('Contribution link already redeemed') )
throw new Error('Contribution link already redeemed')
}
break
}
case ContributionCycleType.DAILY: {
const start = new Date()
start.setHours(0, 0, 0, 0)
const end = new Date()
end.setHours(23, 59, 59, 999)
alreadyRedeemed = await queryRunner.manager
.createQueryBuilder()
.select('contribution')
.from(DbContribution, 'contribution')
.where(
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
{
linkId: contributionLink.id,
id: user.id,
start,
end,
},
)
.getOne()
if (alreadyRedeemed) {
logger.error(
'contribution link with rule DAILY already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed today')
}
break
}
default: {
logger.error('contribution link has unknown cycle', contributionLink.cycle)
throw new Error('Contribution link has unknown cycle')
}
} }
const creations = await getUserCreation(user.id, false) const creations = await getUserCreation(user.id, false)

View File

@ -5,6 +5,8 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/help
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { import {
login,
logout,
createUser, createUser,
setPassword, setPassword,
forgotPassword, forgotPassword,
@ -12,7 +14,7 @@ import {
createContribution, createContribution,
confirmContribution, confirmContribution,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries' import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { User } from '@entity/User' import { User } from '@entity/User'
import CONFIG from '@/config' import CONFIG from '@/config'
@ -358,7 +360,7 @@ describe('UserResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister) await userFactory(testEnv, bobBaumeister)
await query({ query: login, variables: bobData }) await mutate({ mutation: login, variables: bobData })
// create contribution as user bob // create contribution as user bob
contribution = await mutate({ contribution = await mutate({
@ -367,7 +369,7 @@ describe('UserResolver', () => {
}) })
// login as admin // login as admin
await query({ query: login, variables: peterData }) await mutate({ mutation: login, variables: peterData })
// confirm the contribution // confirm the contribution
contribution = await mutate({ contribution = await mutate({
@ -376,7 +378,7 @@ describe('UserResolver', () => {
}) })
// login as user bob // login as user bob
bob = await query({ query: login, variables: bobData }) bob = await mutate({ mutation: login, variables: bobData })
// create transaction link // create transaction link
await transactionLinkFactory(testEnv, { await transactionLinkFactory(testEnv, {
@ -582,7 +584,7 @@ describe('UserResolver', () => {
describe('no users in database', () => { describe('no users in database', () => {
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks() jest.clearAllMocks()
result = await query({ query: login, variables }) result = await mutate({ mutation: login, variables })
}) })
it('throws an error', () => { it('throws an error', () => {
@ -603,7 +605,7 @@ describe('UserResolver', () => {
describe('user is in database and correct login data', () => { describe('user is in database and correct login data', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
result = await query({ query: login, variables }) result = await mutate({ mutation: login, variables })
}) })
afterAll(async () => { afterAll(async () => {
@ -640,7 +642,7 @@ describe('UserResolver', () => {
describe('user is in database and wrong password', () => { describe('user is in database and wrong password', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
result = await query({ query: login, variables: { ...variables, password: 'wrong' } }) result = await mutate({ mutation: login, variables: { ...variables, password: 'wrong' } })
}) })
afterAll(async () => { afterAll(async () => {
@ -665,7 +667,7 @@ describe('UserResolver', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws an error', async () => { it('throws an error', async () => {
resetToken() resetToken()
await expect(query({ query: logout })).resolves.toEqual( await expect(mutate({ mutation: logout })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')], errors: [new GraphQLError('401 Unauthorized')],
}), }),
@ -681,7 +683,7 @@ describe('UserResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await query({ query: login, variables }) await mutate({ mutation: login, variables })
}) })
afterAll(async () => { afterAll(async () => {
@ -689,7 +691,7 @@ describe('UserResolver', () => {
}) })
it('returns true', async () => { it('returns true', async () => {
await expect(query({ query: logout })).resolves.toEqual( await expect(mutate({ mutation: logout })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { logout: 'true' }, data: { logout: 'true' },
errors: undefined, errors: undefined,
@ -738,7 +740,7 @@ describe('UserResolver', () => {
} }
beforeAll(async () => { beforeAll(async () => {
await query({ query: login, variables }) await mutate({ mutation: login, variables })
user = await User.find() user = await User.find()
}) })
@ -929,8 +931,8 @@ describe('UserResolver', () => {
describe('authenticated', () => { describe('authenticated', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await query({ await mutate({
query: login, mutation: login,
variables: { variables: {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
password: 'Aa12345_', password: 'Aa12345_',
@ -1061,8 +1063,8 @@ describe('UserResolver', () => {
it('can login with new password', async () => { it('can login with new password', async () => {
await expect( await expect(
query({ mutate({
query: login, mutation: login,
variables: { variables: {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
password: 'Bb12345_', password: 'Bb12345_',
@ -1081,8 +1083,8 @@ describe('UserResolver', () => {
it('cannot login with old password', async () => { it('cannot login with old password', async () => {
await expect( await expect(
query({ mutate({
query: login, mutation: login,
variables: { variables: {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
password: 'Aa12345_', password: 'Aa12345_',
@ -1119,8 +1121,8 @@ describe('UserResolver', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
await query({ await mutate({
query: login, mutation: login,
variables: { variables: {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
password: 'Aa12345_', password: 'Aa12345_',

View File

@ -316,7 +316,7 @@ export class UserResolver {
} }
@Authorized([RIGHTS.LOGIN]) @Authorized([RIGHTS.LOGIN])
@Query(() => User) @Mutation(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware) @UseMiddleware(klicktippNewsletterStateMiddleware)
async login( async login(
@Args() { email, password, publisherId }: UnsecureLoginArgs, @Args() { email, password, publisherId }: UnsecureLoginArgs,
@ -377,7 +377,7 @@ export class UserResolver {
} }
@Authorized([RIGHTS.LOGOUT]) @Authorized([RIGHTS.LOGOUT])
@Query(() => String) @Mutation(() => String)
async logout(): Promise<boolean> { async logout(): Promise<boolean> {
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token. // TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login) // Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)

View File

@ -1,6 +1,5 @@
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { createContributionLink } from '@/seeds/graphql/mutations' import { login, createContributionLink } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { ContributionLink } from '@model/ContributionLink' import { ContributionLink } from '@model/ContributionLink'
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface' import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
@ -8,12 +7,12 @@ export const contributionLinkFactory = async (
client: ApolloServerTestClient, client: ApolloServerTestClient,
contributionLink: ContributionLinkInterface, contributionLink: ContributionLinkInterface,
): Promise<ContributionLink> => { ): Promise<ContributionLink> => {
const { mutate, query } = client const { mutate } = client
// login as admin // login as admin
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const user = await query({ const user = await mutate({
query: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })

View File

@ -2,8 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations' import { login, adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { CreationInterface } from '@/seeds/creation/CreationInterface' import { CreationInterface } from '@/seeds/creation/CreationInterface'
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { Transaction } from '@entity/Transaction' import { Transaction } from '@entity/Transaction'
@ -19,9 +18,9 @@ export const creationFactory = async (
client: ApolloServerTestClient, client: ApolloServerTestClient,
creation: CreationInterface, creation: CreationInterface,
): Promise<Contribution | void> => { ): Promise<Contribution | void> => {
const { mutate, query } = client const { mutate } = client
logger.trace('creationFactory...') logger.trace('creationFactory...')
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } }) await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
logger.trace('creationFactory... after login') logger.trace('creationFactory... after login')
// TODO it would be nice to have this mutation return the id // TODO it would be nice to have this mutation return the id
await mutate({ mutation: adminCreateContribution, variables: { ...creation } }) await mutate({ mutation: adminCreateContribution, variables: { ...creation } })

View File

@ -1,6 +1,5 @@
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { createTransactionLink } from '@/seeds/graphql/mutations' import { login, createTransactionLink } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface' import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface'
import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver' import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver'
import { TransactionLink } from '@entity/TransactionLink' import { TransactionLink } from '@entity/TransactionLink'
@ -9,10 +8,13 @@ export const transactionLinkFactory = async (
client: ApolloServerTestClient, client: ApolloServerTestClient,
transactionLink: TransactionLinkInterface, transactionLink: TransactionLinkInterface,
): Promise<void> => { ): Promise<void> => {
const { mutate, query } = client const { mutate } = client
// login // login
await query({ query: login, variables: { email: transactionLink.email, password: 'Aa12345_' } }) await mutate({
mutation: login,
variables: { email: transactionLink.email, password: 'Aa12345_' },
})
const variables = { const variables = {
amount: transactionLink.amount, amount: transactionLink.amount,

View File

@ -289,3 +289,33 @@ export const adminCreateContributionMessage = gql`
} }
} }
` `
export const redeemTransactionLink = gql`
mutation ($code: String!) {
redeemTransactionLink(code: $code)
}
`
export const login = gql`
mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
id
email
firstName
lastName
language
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
}
}
`
export const logout = gql`
mutation {
logout
}
`

View File

@ -1,23 +1,5 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export const login = gql`
query ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
id
email
firstName
lastName
language
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
}
}
`
export const verifyLogin = gql` export const verifyLogin = gql`
query { query {
verifyLogin { verifyLogin {
@ -35,12 +17,6 @@ export const verifyLogin = gql`
} }
` `
export const logout = gql`
query {
logout
}
`
export const queryOptIn = gql` export const queryOptIn = gql`
query ($optIn: String!) { query ($optIn: String!) {
queryOptIn(optIn: $optIn) queryOptIn(optIn: $optIn)

View File

@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import Decimal from 'decimal.js-light'
expect.extend({
decimalEqual(received, value) {
const pass = new Decimal(value).equals(received.toString())
if (pass) {
return {
message: () => `expected ${received} to not equal ${value}`,
pass: true,
}
} else {
return {
message: () => `expected ${received} to equal ${value}`,
pass: false,
}
}
},
})
interface CustomMatchers<R = unknown> {
decimalEqual(value: number): R
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}

View File

@ -1668,9 +1668,9 @@ camelcase@^6.2.0:
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001264: caniuse-lite@^1.0.30001264:
version "1.0.30001325" version "1.0.30001418"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz"
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ== integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
chalk@^2.0.0: chalk@^2.0.0:
version "2.4.2" version "2.4.2"

View File

@ -62,13 +62,13 @@ The business events will be stored in database in the new table `EventProtocol`.
The following table lists for each event type the mapping between old and new key, the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table: The following table lists for each event type the mapping between old and new key, the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table:
| EventType - old key | EventType - new key | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount | | EventKey | EventType | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount |
| :-------------------------------- | :------------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: | | :-------------------------------- | :------------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
| BASIC | BasicEvent | x | x | x | | | | | | | | BASIC | BasicEvent | x | x | x | | | | | | |
| VISIT_GRADIDO | VisitGradidoEvent | x | x | x | | | | | | | | VISIT_GRADIDO | VisitGradidoEvent | x | x | x | | | | | | |
| REGISTER | RegisterEvent | x | x | x | x | | | | | | | REGISTER | RegisterEvent | x | x | x | x | | | | | |
| LOGIN | LoginEvent | x | x | x | x | | | | | | | LOGIN | LoginEvent | x | x | x | x | | | | | |
| | VerifyRedeemEvent | | | | | | | | | | | VERIFY_REDEEM | VerifyRedeemEvent | x | x | x | x | | | (x) | (x) | |
| REDEEM_REGISTER | RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | | | REDEEM_REGISTER | RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | |
| REDEEM_LOGIN | RedeemLoginEvent | x | x | x | x | | | (x) | (x) | | | REDEEM_LOGIN | RedeemLoginEvent | x | x | x | x | | | (x) | (x) | |
| ACTIVATE_ACCOUNT | ActivateAccountEvent | x | x | x | x | | | | | | | ACTIVATE_ACCOUNT | ActivateAccountEvent | x | x | x | x | | | | | |
@ -82,20 +82,20 @@ The following table lists for each event type the mapping between old and new ke
| TRANSACTION_SEND_REDEEM | TransactionLinkRedeemEvent | x | x | x | x | x | x | x | | x | | TRANSACTION_SEND_REDEEM | TransactionLinkRedeemEvent | x | x | x | x | x | x | x | | x |
| CONTRIBUTION_CREATE | ContributionCreateEvent | x | x | x | x | | | | x | x | | CONTRIBUTION_CREATE | ContributionCreateEvent | x | x | x | x | | | | x | x |
| CONTRIBUTION_CONFIRM | ContributionConfirmEvent | x | x | x | x | x | x | | x | x | | CONTRIBUTION_CONFIRM | ContributionConfirmEvent | x | x | x | x | x | x | | x | x |
| | ContributionDenyEvent | x | x | x | x | x | x | | x | x | | CONTRIBUTION_DENY | ContributionDenyEvent | x | x | x | x | x | x | | x | x |
| CONTRIBUTION_LINK_DEFINE | ContributionLinkDefineEvent | x | x | x | x | | | | | x | | CONTRIBUTION_LINK_DEFINE | ContributionLinkDefineEvent | x | x | x | x | | | | | x |
| CONTRIBUTION_LINK_ACTIVATE_REDEEM | ContributionLinkRedeemEvent | x | x | x | x | | | | x | x | | CONTRIBUTION_LINK_ACTIVATE_REDEEM | ContributionLinkRedeemEvent | x | x | x | x | | | | x | x |
| | UserCreateContributionMessageEvent | x | x | x | x | | | | x | x | | USER_CREATES_CONTRIBUTION_MESSAGE | UserCreateContributionMessageEvent | x | x | x | x | | | | x | x |
| | AdminCreateContributionMessageEvent | x | x | x | x | | | | x | x | | ADMIN_CREATES_CONTRIBUTION_MESSAGE | AdminCreateContributionMessageEvent | x | x | x | x | | | | x | x |
| | LogoutEvent | x | x | x | x | | | | x | x | | LOGOUT | LogoutEvent | x | x | x | x | | | | | |
| SEND_CONFIRMATION_EMAIL | SendConfirmEmailEvent | x | x | x | x | | | | | | | SEND_CONFIRMATION_EMAIL | SendConfirmEmailEvent | x | x | x | x | | | | | |
| | SendAccountMultiRegistrationEmailEvent | x | x | x | x | | | | | | | SEND_ACCOUNT_MULTIREGISTRATION_EMAIL | SendAccountMultiRegistrationEmailEvent | x | x | x | x | | | | | |
| | SendForgotPasswordEmailEvent | x | x | x | x | | | | | | | SEND_FORGOT_PASSWORD_EMAIL | SendForgotPasswordEmailEvent | x | x | x | x | | | | | |
| | SendTransactionSendEmailEvent | x | x | x | x | x | x | x | | x | | SEND_TRANSACTION_SEND_EMAIL | SendTransactionSendEmailEvent | x | x | x | x | x | x | x | | x |
| | SendTransactionReceiveEmailEvent | x | x | x | x | x | x | x | | x | | SEND_TRANSACTION_RECEIVE_EMAIL | SendTransactionReceiveEmailEvent | x | x | x | x | x | x | x | | x |
| | SendAddedContributionEmailEvent | x | x | x | x | | | | x | x | | SEND_ADDED_CONTRIBUTION_EMAIL | SendAddedContributionEmailEvent | x | x | x | x | | | | x | x |
| | SendContributionConfirmEmailEvent | x | x | x | x | | | | x | x | | SEND_CONTRIBUTION_CONFIRM_EMAIL | SendContributionConfirmEmailEvent | x | x | x | x | | | | x | x |
| | SendTransactionLinkRedeemEmailEvent | x | x | x | x | x | x | x | | x | | SEND_TRANSACTION_LINK_REDEEM_EMAIL | SendTransactionLinkRedeemEmailEvent | x | x | x | x | x | x | x | | x |
| TRANSACTION_REPEATE_REDEEM | - | | | | | | | | | | | TRANSACTION_REPEATE_REDEEM | - | | | | | | | | | |
| TRANSACTION_RECEIVE_REDEEM | - | | | | | | | | | | | TRANSACTION_RECEIVE_REDEEM | - | | | | | | | | | |

View File

@ -25,6 +25,7 @@ describe('App', () => {
meta: { meta: {
requiresAuth: false, requiresAuth: false,
}, },
params: {},
}, },
} }

View File

@ -18,9 +18,9 @@
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img> <b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
<b-collapse id="nav-collapse" is-nav class="ml-5"> <b-collapse id="nav-collapse" is-nav class="ml-5">
<b-navbar-nav class="ml-auto d-none d-lg-flex" right> <b-navbar-nav class="ml-auto d-none d-lg-flex" right>
<b-nav-item to="/register" class="authNavbar ml-lg-5">{{ $t('signup') }}</b-nav-item> <b-nav-item :to="register" class="authNavbar ml-lg-5">{{ $t('signup') }}</b-nav-item>
<span class="d-none d-lg-block mt-1">{{ $t('math.pipe') }}</span> <span class="d-none d-lg-block mt-1">{{ $t('math.pipe') }}</span>
<b-nav-item to="/login" class="authNavbar">{{ $t('signin') }}</b-nav-item> <b-nav-item :to="login" class="authNavbar">{{ $t('signin') }}</b-nav-item>
</b-navbar-nav> </b-navbar-nav>
</b-collapse> </b-collapse>
</b-navbar> </b-navbar>
@ -28,8 +28,11 @@
</template> </template>
<script> <script>
import { authLinks } from '@/mixins/authLinks'
export default { export default {
name: 'AuthNavbar', name: 'AuthNavbar',
mixins: [authLinks],
data() { data() {
return { return {
logo: '/img/brand/green.png', logo: '/img/brand/green.png',

View File

@ -2,17 +2,20 @@
<div class="navbar-small"> <div class="navbar-small">
<b-navbar class="navi"> <b-navbar class="navi">
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/register" class="authNavbar">{{ $t('signup') }}</b-nav-item> <b-nav-item :to="register" class="authNavbar">{{ $t('signup') }}</b-nav-item>
<span class="mt-1">{{ $t('math.pipe') }}</span> <span class="mt-1">{{ $t('math.pipe') }}</span>
<b-nav-item to="/login" class="authNavbar">{{ $t('signin') }}</b-nav-item> <b-nav-item :to="login" class="authNavbar">{{ $t('signin') }}</b-nav-item>
</b-navbar-nav> </b-navbar-nav>
</b-navbar> </b-navbar>
</div> </div>
</template> </template>
<script> <script>
import { authLinks } from '@/mixins/authLinks'
export default { export default {
name: 'AuthNavbarSmall', name: 'AuthNavbarSmall',
mixins: [authLinks],
} }
</script> </script>
<style scoped> <style scoped>

View File

@ -329,7 +329,8 @@ describe('ContributionForm', () => {
describe('invalid form data', () => { describe('invalid form data', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) // skip this precondition as long as the datepicker is disabled in the component
// await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now)
await wrapper.find('#contribution-amount').find('input').setValue('200') await wrapper.find('#contribution-amount').find('input').setValue('200')
}) })

View File

@ -25,6 +25,7 @@
reset-value="" reset-value=""
:label-no-date-selected="$t('contribution.noDateSelected')" :label-no-date-selected="$t('contribution.noDateSelected')"
required required
:disabled="this.form.id !== null"
> >
<template #nav-prev-year><span></span></template> <template #nav-prev-year><span></span></template>
<template #nav-next-year><span></span></template> <template #nav-next-year><span></span></template>

View File

@ -25,9 +25,11 @@
</template> </template>
<script> <script>
import RedeemInformation from '@/components/LinkInformations/RedeemInformation.vue' import RedeemInformation from '@/components/LinkInformations/RedeemInformation.vue'
import { authLinks } from '@/mixins/authLinks'
export default { export default {
name: 'RedeemLoggedOut', name: 'RedeemLoggedOut',
mixins: [authLinks],
components: { components: {
RedeemInformation, RedeemInformation,
}, },
@ -35,13 +37,5 @@ export default {
linkData: { type: Object, required: true }, linkData: { type: Object, required: true },
isContributionLink: { type: Boolean, default: false }, isContributionLink: { type: Boolean, default: false },
}, },
computed: {
login() {
return '/login/' + this.$route.params.code
},
register() {
return '/register/' + this.$route.params.code
},
},
} }
</script> </script>

View File

@ -136,3 +136,27 @@ export const createContributionMessage = gql`
} }
} }
` `
export const login = gql`
mutation($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
lastName
language
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
creation
}
}
`
export const logout = gql`
mutation {
logout
}
`

View File

@ -1,23 +1,5 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export const login = gql`
query($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
lastName
language
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
creation
}
}
`
export const verifyLogin = gql` export const verifyLogin = gql`
query { query {
verifyLogin { verifyLogin {
@ -36,12 +18,6 @@ export const verifyLogin = gql`
} }
` `
export const logout = gql`
query {
logout
}
`
export const transactionsQuery = gql` export const transactionsQuery = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) { transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {

View File

@ -19,6 +19,7 @@ describe('AuthLayout', () => {
meta: { meta: {
requiresAuth: false, requiresAuth: false,
}, },
params: {},
}, },
} }

View File

@ -18,6 +18,7 @@ const apolloMock = jest.fn().mockResolvedValue({
logout: 'success', logout: 'success',
}, },
}) })
const apolloQueryMock = jest.fn()
describe('DashboardLayout', () => { describe('DashboardLayout', () => {
let wrapper let wrapper
@ -40,7 +41,8 @@ describe('DashboardLayout', () => {
}, },
}, },
$apollo: { $apollo: {
query: apolloMock, mutate: apolloMock,
query: apolloQueryMock,
}, },
$store: { $store: {
state: { state: {
@ -142,7 +144,7 @@ describe('DashboardLayout', () => {
describe('update transactions', () => { describe('update transactions', () => {
beforeEach(async () => { beforeEach(async () => {
apolloMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
transactionList: { transactionList: {
balance: { balance: {
@ -163,7 +165,7 @@ describe('DashboardLayout', () => {
}) })
it('calls the API', () => { it('calls the API', () => {
expect(apolloMock).toBeCalledWith( expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
currentPage: 2, currentPage: 2,
@ -201,7 +203,7 @@ describe('DashboardLayout', () => {
describe('update transactions returns error', () => { describe('update transactions returns error', () => {
beforeEach(async () => { beforeEach(async () => {
apolloMock.mockRejectedValue({ apolloQueryMock.mockRejectedValue({
message: 'Ouch!', message: 'Ouch!',
}) })
await wrapper await wrapper

View File

@ -41,7 +41,8 @@
import Navbar from '@/components/Menu/Navbar.vue' import Navbar from '@/components/Menu/Navbar.vue'
import Sidebar from '@/components/Menu/Sidebar.vue' import Sidebar from '@/components/Menu/Sidebar.vue'
import SessionLogoutTimeout from '@/components/SessionLogoutTimeout.vue' import SessionLogoutTimeout from '@/components/SessionLogoutTimeout.vue'
import { logout, transactionsQuery } from '@/graphql/queries' import { transactionsQuery } from '@/graphql/queries'
import { logout } from '@/graphql/mutations'
import ContentFooter from '@/components/ContentFooter.vue' import ContentFooter from '@/components/ContentFooter.vue'
import { FadeTransition } from 'vue2-transitions' import { FadeTransition } from 'vue2-transitions'
import CONFIG from '@/config' import CONFIG from '@/config'
@ -75,8 +76,8 @@ export default {
methods: { methods: {
async logout() { async logout() {
this.$apollo this.$apollo
.query({ .mutate({
query: logout, mutation: logout,
}) })
.then(() => { .then(() => {
this.$store.dispatch('logout') this.$store.dispatch('logout')

View File

@ -24,7 +24,8 @@
"moderator": "Moderator", "moderator": "Moderator",
"moderators": "Moderatoren", "moderators": "Moderatoren",
"myContributions": "Meine Beiträge zum Gemeinwohl", "myContributions": "Meine Beiträge zum Gemeinwohl",
"openContributionLinks": "öffentliche Beitrags-Linkliste", "noOpenContributionLinkText": "Zur Zeit gibt es keine automatischen Schöpfungen.",
"openContributionLinks": "Öffentliche Beitrags-Linkliste",
"openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.", "openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.",
"other-communities": "Weitere Gemeinschaften", "other-communities": "Weitere Gemeinschaften",
"submitContribution": "Beitrag einreichen", "submitContribution": "Beitrag einreichen",

View File

@ -24,7 +24,8 @@
"moderator": "Moderator", "moderator": "Moderator",
"moderators": "Moderators", "moderators": "Moderators",
"myContributions": "My contributions to the common good", "myContributions": "My contributions to the common good",
"openContributionLinks": "open Contribution links list", "noOpenContributionLinkText": "Currently there are no automatic creations.",
"openContributionLinks": "Open contribution-link list",
"openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.", "openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.",
"other-communities": "Other communities", "other-communities": "Other communities",
"submitContribution": "Submit contribution", "submitContribution": "Submit contribution",

View File

@ -0,0 +1,12 @@
export const authLinks = {
computed: {
login() {
if (this.$route.params.code) return '/login/' + this.$route.params.code
return '/login'
},
register() {
if (this.$route.params.code) return '/register/' + this.$route.params.code
return '/register'
},
},
}

View File

@ -14,7 +14,7 @@
<hr /> <hr />
<b-container> <b-container>
<div class="h3">{{ $t('community.openContributionLinks') }}</div> <div class="h3">{{ $t('community.openContributionLinks') }}</div>
<small> <small v-if="count > 0">
{{ {{
$t('community.openContributionLinkText', { $t('community.openContributionLinkText', {
name: CONFIG.COMMUNITY_NAME, name: CONFIG.COMMUNITY_NAME,
@ -22,6 +22,9 @@
}) })
}} }}
</small> </small>
<small v-else>
{{ $t('community.noOpenContributionLinkText') }}
</small>
<ul> <ul>
<li v-for="item in itemsContributionLinks" v-bind:key="item.id"> <li v-for="item in itemsContributionLinks" v-bind:key="item.id">
<div>{{ item.name }}</div> <div>{{ item.name }}</div>

View File

@ -5,7 +5,7 @@ import Login from './Login'
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest.fn() const apolloMutateMock = jest.fn()
const mockStoreDispach = jest.fn() const mockStoreDispach = jest.fn()
const mockStoreCommit = jest.fn() const mockStoreCommit = jest.fn()
const mockRouterPush = jest.fn() const mockRouterPush = jest.fn()
@ -41,7 +41,7 @@ describe('Login', () => {
params: {}, params: {},
}, },
$apollo: { $apollo: {
query: apolloQueryMock, mutate: apolloMutateMock,
}, },
} }
@ -113,7 +113,7 @@ describe('Login', () => {
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org') await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234') await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises() await flushPromises()
apolloQueryMock.mockResolvedValue({ apolloMutateMock.mockResolvedValue({
data: { data: {
login: 'token', login: 'token',
}, },
@ -123,7 +123,7 @@ describe('Login', () => {
}) })
it('calls the API with the given data', () => { it('calls the API with the given data', () => {
expect(apolloQueryMock).toBeCalledWith( expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
email: 'user@example.org', email: 'user@example.org',
@ -175,7 +175,7 @@ describe('Login', () => {
describe('login fails', () => { describe('login fails', () => {
const createError = async (errorMessage) => { const createError = async (errorMessage) => {
apolloQueryMock.mockRejectedValue({ apolloMutateMock.mockRejectedValue({
message: errorMessage, message: errorMessage,
}) })
wrapper = Wrapper() wrapper = Wrapper()

View File

@ -43,7 +43,7 @@
import InputPassword from '@/components/Inputs/InputPassword' import InputPassword from '@/components/Inputs/InputPassword'
import InputEmail from '@/components/Inputs/InputEmail' import InputEmail from '@/components/Inputs/InputEmail'
import Message from '@/components/Message/Message' import Message from '@/components/Message/Message'
import { login } from '@/graphql/queries' import { login } from '@/graphql/mutations'
export default { export default {
name: 'Login', name: 'Login',
@ -71,14 +71,13 @@ export default {
container: this.$refs.submitButton, container: this.$refs.submitButton,
}) })
this.$apollo this.$apollo
.query({ .mutate({
query: login, mutation: login,
variables: { variables: {
email: this.form.email, email: this.form.email,
password: this.form.password, password: this.form.password,
publisherId: this.$store.state.publisherId, publisherId: this.$store.state.publisherId,
}, },
fetchPolicy: 'network-only',
}) })
.then(async (result) => { .then(async (result) => {
const { const {

View File

@ -43,6 +43,7 @@ const mocks = {
$store: { $store: {
state: { state: {
token: null, token: null,
tokenTime: null,
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
}, },
}, },
@ -68,7 +69,7 @@ describe('TransactionLink', () => {
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeAll(() => {
jest.clearAllMocks() jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
}) })
@ -214,112 +215,159 @@ describe('TransactionLink', () => {
}) })
}) })
describe('token in store and own link', () => { describe('token in store', () => {
beforeEach(() => { beforeAll(() => {
mocks.$store.state.token = 'token' mocks.$store.state.token = 'token'
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
__typename: 'TransactionLink',
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
}) })
it('has a RedeemSelfCreator component', () => { describe('sufficient token time in store', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true) beforeAll(() => {
}) mocks.$store.state.tokenTime = Math.floor(Date.now() / 1000) + 20
it('has a no redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain(
'gdd_per_link.no-redeem',
)
})
it.skip('has a link to transaction page', () => {
expect(wrapper.find('a[target="/transactions"]').exists()).toBe(true)
})
})
describe('valid link', () => {
beforeEach(() => {
mocks.$store.state.token = 'token'
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
__typename: 'TransactionLink',
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' },
},
},
})
wrapper = Wrapper()
})
it('has a RedeemValid component', () => {
expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true)
})
it('has a button with redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe(
'gdd_per_link.redeem',
)
})
describe('redeem link with success', () => {
beforeEach(async () => {
apolloMutateMock.mockResolvedValue()
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
}) })
it('calls the API', () => { describe('own link', () => {
expect(apolloMutateMock).toBeCalledWith( beforeAll(() => {
expect.objectContaining({ apolloQueryMock.mockResolvedValue({
mutation: redeemTransactionLink, data: {
variables: { queryTransactionLink: {
code: 'some-code', __typename: 'TransactionLink',
id: 92,
amount: '22',
memo:
'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
}, },
}), })
) wrapper = Wrapper()
})
it('has a RedeemSelfCreator component', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true)
})
it('has a no redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain(
'gdd_per_link.no-redeem',
)
})
it.skip('has a link to transaction page', () => {
expect(wrapper.find('a[target="/transactions"]').exists()).toBe(true)
})
}) })
it('toasts a success message', () => { describe('valid link', () => {
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem') beforeAll(() => {
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ') apolloQueryMock.mockResolvedValue({
}) data: {
queryTransactionLink: {
__typename: 'TransactionLink',
id: 92,
amount: '22',
memo:
'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' },
},
},
})
wrapper = Wrapper()
})
it('pushes the route to overview', () => { it('has a RedeemValid component', () => {
expect(routerPushMock).toBeCalledWith('/overview') expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true)
})
it('has a button with redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe(
'gdd_per_link.redeem',
)
})
describe('redeem link with success', () => {
beforeAll(async () => {
apolloMutateMock.mockResolvedValue()
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: redeemTransactionLink,
variables: {
code: 'some-code',
},
}),
)
})
it('toasts a success message', () => {
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem')
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
})
it('pushes the route to overview', () => {
expect(routerPushMock).toBeCalledWith('/overview')
})
})
describe('redeem link with error', () => {
beforeAll(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' })
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh Noo!')
})
it('pushes the route to overview', () => {
expect(routerPushMock).toBeCalledWith('/overview')
})
})
}) })
}) })
describe('redeem link with error', () => { describe('no sufficient token time in store', () => {
beforeEach(async () => { beforeAll(() => {
apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) mocks.$store.state.tokenTime = 1665125185
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
__typename: 'TransactionLink',
id: 92,
amount: '22',
memo:
'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
}) })
it('toasts an error message', () => { it('has a RedeemLoggedOut component', () => {
expect(toastErrorSpy).toBeCalledWith('Oh Noo!') expect(wrapper.findComponent({ name: 'RedeemLoggedOut' }).exists()).toBe(true)
}) })
it('pushes the route to overview', () => { it('has a link to register with code', () => {
expect(routerPushMock).toBeCalledWith('/overview') expect(wrapper.find('a[href="/register/some-code"]').exists()).toBe(true)
})
it('has a link to login with code', () => {
expect(wrapper.find('a[href="/login/some-code"]').exists()).toBe(true)
}) })
}) })
}) })

View File

@ -103,6 +103,12 @@ export default {
isContributionLink() { isContributionLink() {
return this.$route.params.code.search(/^CL-/) === 0 return this.$route.params.code.search(/^CL-/) === 0
}, },
tokenExpiresInSeconds() {
const remainingSecs = Math.floor(
(new Date(this.$store.state.tokenTime * 1000).getTime() - new Date().getTime()) / 1000,
)
return remainingSecs <= 0 ? 0 : remainingSecs
},
itemType() { itemType() {
// link is deleted: at, from // link is deleted: at, from
if (this.linkData.deletedAt) { if (this.linkData.deletedAt) {
@ -130,7 +136,9 @@ export default {
return `TEXT` return `TEXT`
} }
if (this.$store.state.token) { if (this.$store.state.token && this.$store.state.tokenTime) {
if (this.tokenExpiresInSeconds < 5) return `LOGGED_OUT`
// logged in, nicht berechtigt einzulösen, eigener link // logged in, nicht berechtigt einzulösen, eigener link
if (this.linkData.user && this.$store.state.email === this.linkData.user.email) { if (this.linkData.user && this.$store.state.email === this.linkData.user.email) {
return `SELF_CREATOR` return `SELF_CREATOR`