Merge branch 'master' into refactor_arithmetic_transactionlist_adminarea

This commit is contained in:
Ulf Gebhardt 2022-03-04 14:43:39 +01:00 committed by GitHub
commit 91fda4c097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 234 additions and 218 deletions

View File

@ -36,6 +36,7 @@
"graphql": "^15.6.1", "graphql": "^15.6.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "26.6.3", "jest": "26.6.3",
"portal-vue": "^2.1.7",
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9",
"stats-webpack-plugin": "^0.7.0", "stats-webpack-plugin": "^0.7.0",
"vue": "^2.6.11", "vue": "^2.6.11",
@ -43,7 +44,6 @@
"vue-i18n": "^8.26.5", "vue-i18n": "^8.26.5",
"vue-jest": "^3.0.7", "vue-jest": "^3.0.7",
"vue-router": "^3.5.3", "vue-router": "^3.5.3",
"vue-toasted": "^1.1.28",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"vuex-persistedstate": "^4.1.0" "vuex-persistedstate": "^4.1.0"
}, },

View File

@ -1,21 +1,17 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue' import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue() const apolloMutateMock = jest.fn().mockResolvedValue()
const toastSuccessMock = jest.fn()
const toastErrorMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$apollo: { $apollo: {
mutate: apolloMutateMock, mutate: apolloMutateMock,
}, },
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
} }
const propsData = { const propsData = {
@ -54,7 +50,7 @@ describe('ConfirmRegisterMailFormular', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success') expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success')
}) })
}) })
@ -66,7 +62,7 @@ describe('ConfirmRegisterMailFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('unregister_mail.error') expect(toastErrorSpy).toBeCalledWith('unregister_mail.error')
}) })
}) })
}) })

View File

@ -48,10 +48,10 @@ export default {
}, },
}) })
.then(() => { .then(() => {
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email })) this.toastSuccess(this.$t('unregister_mail.success', { email: this.email }))
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message })) this.toastError(this.$t('unregister_mail.error', { message: error.message }))
}) })
}, },
}, },

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationFormular from './CreationFormular.vue' import CreationFormular from './CreationFormular.vue'
import { createPendingCreation } from '../graphql/createPendingCreation' import { createPendingCreation } from '../graphql/createPendingCreation'
import { createPendingCreations } from '../graphql/createPendingCreations' import { createPendingCreations } from '../graphql/createPendingCreations'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -11,8 +12,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
}, },
}) })
const stateCommitMock = jest.fn() const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t, options) => (options ? [t, options] : t)), $t: jest.fn((t, options) => (options ? [t, options] : t)),
@ -32,10 +31,6 @@ const mocks = {
}, },
}, },
}, },
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
} }
const propsData = { const propsData = {
@ -140,7 +135,7 @@ describe('CreationFormular', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith([ expect(toastSuccessSpy).toBeCalledWith([
'creation_form.toasted', 'creation_form.toasted',
{ email: 'benjamin@bluemchen.de', value: '90' }, { email: 'benjamin@bluemchen.de', value: '90' },
]) ])
@ -162,7 +157,7 @@ describe('CreationFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!') expect(toastErrorSpy).toBeCalledWith('Ouch!')
}) })
}) })
@ -292,7 +287,7 @@ describe('CreationFormular', () => {
}) })
it('toast success message', () => { it('toast success message', () => {
expect(toastedSuccessMock).toBeCalled() expect(toastSuccessSpy).toBeCalled()
}) })
it('store commit openCreationPlus', () => { it('store commit openCreationPlus', () => {
@ -426,13 +421,14 @@ describe('CreationFormular', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0) expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
}) })
it('toasts two errors', () => { it('emits remove all bookmarks', () => {
expect(toastedErrorMock).toBeCalledWith( expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
'Could not created PendingCreation for bob@baumeister.de', })
)
expect(toastedErrorMock).toBeCalledWith( it('emits toast failed creations with two emails', () => {
'Could not created PendingCreation for bibi@bloxberg.de', expect(wrapper.emitted('toast-failed-creations')).toEqual([
) [['bob@baumeister.de', 'bibi@bloxberg.de']],
])
}) })
}) })
@ -454,7 +450,7 @@ describe('CreationFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!') expect(toastErrorSpy).toBeCalledWith('Oh no!')
}) })
}) })
}) })

View File

@ -166,20 +166,21 @@ export default {
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
}) })
.then((result) => { .then((result) => {
const failedCreations = []
this.$store.commit( this.$store.commit(
'openCreationsPlus', 'openCreationsPlus',
result.data.createPendingCreations.successfulCreation.length, result.data.createPendingCreations.successfulCreation.length,
) )
if (result.data.createPendingCreations.failedCreation.length > 0) { if (result.data.createPendingCreations.failedCreation.length > 0) {
result.data.createPendingCreations.failedCreation.forEach((failed) => { result.data.createPendingCreations.failedCreation.forEach((email) => {
// TODO: Please localize this error message failedCreations.push(email)
this.$toasted.error('Could not created PendingCreation for ' + failed)
}) })
} }
this.$emit('remove-all-bookmark') this.$emit('remove-all-bookmark')
this.$emit('toast-failed-creations', failedCreations)
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
} else if (this.type === 'singleCreation') { } else if (this.type === 'singleCreation') {
submitObj = { submitObj = {
@ -196,19 +197,19 @@ export default {
}) })
.then((result) => { .then((result) => {
this.$emit('update-user-data', this.item, result.data.createPendingCreation) this.$emit('update-user-data', this.item, result.data.createPendingCreation)
this.$toasted.success( this.$store.commit('openCreationsPlus', 1)
this.toastSuccess(
this.$t('creation_form.toasted', { this.$t('creation_form.toasted', {
value: this.value, value: this.value,
email: this.item.email, email: this.item.email,
}), }),
) )
this.$store.commit('openCreationsPlus', 1)
// what is this? Tests says that this.text is not reseted // what is this? Tests says that this.text is not reseted
this.$refs.creationForm.reset() this.$refs.creationForm.reset()
this.value = 0 this.value = 0
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
this.$refs.creationForm.reset() this.$refs.creationForm.reset()
this.value = 0 this.value = 0
}) })

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import CreationTransactionListFormular from './CreationTransactionListFormular.vue' import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -50,17 +51,12 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
}, },
}) })
const toastedErrorMock = jest.fn()
const mocks = { const mocks = {
$d: jest.fn((t) => t), $d: jest.fn((t) => t),
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },
$toasted: {
error: toastedErrorMock,
},
} }
const propsData = { const propsData = {
@ -109,7 +105,7 @@ describe('CreationTransactionListFormular', () => {
}) })
it('toast error', () => { it('toast error', () => {
expect(toastedErrorMock).toBeCalledWith('OUCH!') expect(toastErrorSpy).toBeCalledWith('OUCH!')
}) })
}) })
}) })

View File

@ -63,7 +63,7 @@ export default {
this.items = result.data.transactionList.transactions.filter((t) => t.type === 'creation') this.items = result.data.transactionList.transactions.filter((t) => t.type === 'creation')
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
}, },

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import DeletedUserFormular from './DeletedUserFormular.vue' import DeletedUserFormular from './DeletedUserFormular.vue'
import { deleteUser } from '../graphql/deleteUser' import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser' import { unDeleteUser } from '../graphql/unDeleteUser'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -13,9 +14,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
}, },
}) })
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$apollo: { $apollo: {
@ -29,10 +27,6 @@ const mocks = {
}, },
}, },
}, },
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
} }
const propsData = { const propsData = {
@ -118,10 +112,6 @@ describe('DeletedUserFormular', () => {
) )
}) })
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('user_deleted')
})
it('emits update deleted At', () => { it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual( expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@ -147,7 +137,7 @@ describe('DeletedUserFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!') expect(toastErrorSpy).toBeCalledWith('Oh no!')
}) })
}) })
@ -215,10 +205,6 @@ describe('DeletedUserFormular', () => {
) )
}) })
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('user_recovered')
})
it('emits update deleted At', () => { it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual( expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@ -244,7 +230,7 @@ describe('DeletedUserFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!') expect(toastErrorSpy).toBeCalledWith('Oh no!')
}) })
}) })

View File

@ -45,7 +45,6 @@ export default {
}, },
}) })
.then((result) => { .then((result) => {
this.$toasted.success(this.$t('user_deleted'))
this.$emit('updateDeletedAt', { this.$emit('updateDeletedAt', {
userId: this.item.userId, userId: this.item.userId,
deletedAt: result.data.deleteUser, deletedAt: result.data.deleteUser,
@ -53,7 +52,7 @@ export default {
this.checked = false this.checked = false
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
unDeleteUser() { unDeleteUser() {
@ -65,7 +64,7 @@ export default {
}, },
}) })
.then((result) => { .then((result) => {
this.$toasted.success(this.$t('user_recovered')) this.toastSuccess(this.$t('user_recovered'))
this.$emit('updateDeletedAt', { this.$emit('updateDeletedAt', {
userId: this.item.userId, userId: this.item.userId,
deletedAt: result.data.unDeleteUser, deletedAt: result.data.unDeleteUser,
@ -73,7 +72,7 @@ export default {
this.checked = false this.checked = false
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
}, },

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import EditCreationFormular from './EditCreationFormular.vue' import EditCreationFormular from './EditCreationFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -16,8 +17,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
}) })
const stateCommitMock = jest.fn() const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
@ -37,10 +36,6 @@ const mocks = {
}, },
commit: stateCommitMock, commit: stateCommitMock,
}, },
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
} }
const now = new Date(Date.now()) const now = new Date(Date.now())
@ -142,7 +137,7 @@ describe('EditCreationFormular', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_update') expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_update')
}) })
}) })
@ -155,7 +150,7 @@ describe('EditCreationFormular', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!') expect(toastErrorSpy).toBeCalledWith('Oh no!')
}) })
}) })
}) })

View File

@ -132,7 +132,7 @@ export default {
moderator: Number(result.data.updatePendingCreation.moderator), moderator: Number(result.data.updatePendingCreation.moderator),
row: this.row, row: this.row,
}) })
this.$toasted.success( this.toastSuccess(
this.$t('creation_form.toasted_update', { this.$t('creation_form.toasted_update', {
value: this.value, value: this.value,
email: this.item.email, email: this.item.email,
@ -144,7 +144,7 @@ export default {
this.value = 0 this.value = 0
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
// das creation Formular reseten // das creation Formular reseten
this.$refs.updateCreationForm.reset() this.$refs.updateCreationForm.reset()
// Den geschöpften Wert auf o setzen // Den geschöpften Wert auf o setzen

View File

@ -73,10 +73,6 @@ const mocks = {
}, },
}, },
}, },
$toasted: {
error: jest.fn(),
success: jest.fn(),
},
} }
describe('SearchUserTable', () => { describe('SearchUserTable', () => {

View File

@ -5,6 +5,7 @@
"confirmed": "bestätigt", "confirmed": "bestätigt",
"creation": "Schöpfung", "creation": "Schöpfung",
"creation_form": { "creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für", "creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen", "enter_text": "Text eintragen",
"form": "Schöpfungsformular", "form": "Schöpfungsformular",
@ -28,6 +29,7 @@
"delete_user": "Nutzer löschen", "delete_user": "Nutzer löschen",
"details": "Details", "details": "Details",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"error": "Fehler",
"e_mail": "E-Mail", "e_mail": "E-Mail",
"firstname": "Vorname", "firstname": "Vorname",
"gradido_admin_footer": "Gradido Akademie Adminkonsole", "gradido_admin_footer": "Gradido Akademie Adminkonsole",
@ -68,6 +70,7 @@
"remove_all": "alle Nutzer entfernen", "remove_all": "alle Nutzer entfernen",
"save": "Speichern", "save": "Speichern",
"status": "Status", "status": "Status",
"success": "Erfolg",
"text": "Text", "text": "Text",
"transaction": "Transaktion", "transaction": "Transaktion",
"transactionlist": { "transactionlist": {

View File

@ -5,6 +5,7 @@
"confirmed": "confirmed", "confirmed": "confirmed",
"creation": "Creation", "creation": "Creation",
"creation_form": { "creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for", "creation_for": "Active Basic Income for",
"enter_text": "Enter text", "enter_text": "Enter text",
"form": "Creation form", "form": "Creation form",
@ -28,6 +29,7 @@
"delete_user": "Delete user", "delete_user": "Delete user",
"details": "Details", "details": "Details",
"edit": "Edit", "edit": "Edit",
"error": "Error",
"e_mail": "E-mail", "e_mail": "E-mail",
"firstname": "Firstname", "firstname": "Firstname",
"gradido_admin_footer": "Gradido Academy Admin Console", "gradido_admin_footer": "Gradido Academy Admin Console",
@ -68,6 +70,7 @@
"remove_all": "Remove all users", "remove_all": "Remove all users",
"save": "Speichern", "save": "Speichern",
"status": "Status", "status": "Status",
"success": "Success",
"text": "Text", "text": "Text",
"transaction": "Transaction", "transaction": "Transaction",
"transactionlist": { "transactionlist": {

View File

@ -13,31 +13,24 @@ import i18n from './i18n'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import PortalVue from 'portal-vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import Toasted from 'vue-toasted' import { toasters } from './mixins/toaster'
import { apolloProvider } from './plugins/apolloProvider' import { apolloProvider } from './plugins/apolloProvider'
Vue.use(PortalVue)
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
Vue.use(IconsPlugin) Vue.use(IconsPlugin)
Vue.use(VueApollo) Vue.use(VueApollo)
Vue.use(Toasted, { Vue.mixin(toasters)
position: 'top-center',
duration: 5000,
fullWidth: true,
action: {
text: 'x',
onClick: (e, toastObject) => {
toastObject.goAway(0)
},
},
})
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n) addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)

View File

@ -0,0 +1,30 @@
export const toasters = {
methods: {
toastSuccess(message) {
this.toast(message, {
title: this.$t('success'),
variant: 'success',
})
},
toastError(message) {
this.toast(message, {
title: this.$t('error'),
variant: 'danger',
})
},
toast(message, options) {
// for unit tests, check that replace is present
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,
solid: true,
toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title',
bodyClass: 'gdd-toaster-body',
toastClass: 'gdd-toaster',
...options,
})
},
},
}

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Creation from './Creation.vue' import Creation from './Creation.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -29,18 +30,14 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
}, },
}) })
const toastErrorMock = jest.fn()
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },
$toasted: {
error: toastErrorMock,
},
$store: { $store: {
commit: storeCommitMock, commit: storeCommitMock,
state: { state: {
@ -236,6 +233,25 @@ describe('Creation', () => {
}) })
}) })
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('watchers', () => { describe('watchers', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
@ -298,7 +314,7 @@ describe('Creation', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch') expect(toastErrorSpy).toBeCalledWith('Ouch')
}) })
}) })
}) })

View File

@ -56,6 +56,7 @@
:creation="creation" :creation="creation"
:items="itemsMassCreation" :items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmarks" @remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/> />
</b-col> </b-col>
</b-row> </b-row>
@ -118,7 +119,7 @@ export default {
} }
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
pushItem(selectedItem) { pushItem(selectedItem) {
@ -144,6 +145,11 @@ export default {
this.$store.commit('setUserSelectedInMassCreation', []) this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers() this.getUsers()
}, },
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
}, },
computed: { computed: {
Searchfields() { Searchfields() {

View File

@ -2,12 +2,11 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue' import CreationConfirm from './CreationConfirm.vue'
import { deletePendingCreation } from '../graphql/deletePendingCreation' import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation' import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
getPendingCreations: [ getPendingCreations: [
@ -47,10 +46,6 @@ const mocks = {
query: apolloQueryMock, query: apolloQueryMock,
mutate: apolloMutateMock, mutate: apolloMutateMock,
}, },
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
} }
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
@ -101,7 +96,7 @@ describe('CreationConfirm', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_delete') expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
}) })
}) })
@ -112,7 +107,7 @@ describe('CreationConfirm', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouchhh!') expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
}) })
}) })
@ -158,7 +153,7 @@ describe('CreationConfirm', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created') expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
}) })
it('has 1 item left in the table', () => { it('has 1 item left in the table', () => {
@ -173,7 +168,7 @@ describe('CreationConfirm', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouchhh!') expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
}) })
}) })
}) })
@ -189,7 +184,7 @@ describe('CreationConfirm', () => {
}) })
it('toast an error message', () => { it('toast an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!') expect(toastErrorSpy).toBeCalledWith('Ouch!')
}) })
}) })
}) })

View File

@ -43,10 +43,10 @@ export default {
}) })
.then((result) => { .then((result) => {
this.updatePendingCreations(item.id) this.updatePendingCreations(item.id)
this.$toasted.success(this.$t('creation_form.toasted_delete')) this.toastSuccess(this.$t('creation_form.toasted_delete'))
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
confirmCreation() { confirmCreation() {
@ -60,11 +60,11 @@ export default {
.then((result) => { .then((result) => {
this.overlay = false this.overlay = false
this.updatePendingCreations(this.item.id) this.updatePendingCreations(this.item.id)
this.$toasted.success(this.$t('creation_form.toasted_created')) this.toastSuccess(this.$t('creation_form.toasted_created'))
}) })
.catch((error) => { .catch((error) => {
this.overlay = false this.overlay = false
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
getPendingCreations() { getPendingCreations() {
@ -79,7 +79,7 @@ export default {
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length) this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
updatePendingCreations(id) { updatePendingCreations(id) {

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue' import UserSearch from './UserSearch.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
@ -15,6 +16,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
creation: [200, 400, 600], creation: [200, 400, 600],
emailChecked: true, emailChecked: true,
deletedAt: null,
}, },
{ {
userId: 2, userId: 2,
@ -23,6 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'benjamin@bluemchen.de', email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000], creation: [1000, 1000, 1000],
emailChecked: true, emailChecked: true,
deletedAt: null,
}, },
{ {
userId: 3, userId: 3,
@ -31,6 +34,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'peter@lustig.de', email: 'peter@lustig.de',
creation: [0, 0, 0], creation: [0, 0, 0],
emailChecked: true, emailChecked: true,
deletedAt: null,
}, },
{ {
userId: 4, userId: 4,
@ -39,23 +43,19 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'new@user.ch', email: 'new@user.ch',
creation: [1000, 1000, 1000], creation: [1000, 1000, 1000],
emailChecked: false, emailChecked: false,
deletedAt: null,
}, },
], ],
}, },
}, },
}) })
const toastErrorMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => String(d)), $d: jest.fn((d) => String(d)),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },
$toasted: {
error: toastErrorMock,
},
} }
describe('UserSearch', () => { describe('UserSearch', () => {
@ -187,6 +187,21 @@ describe('UserSearch', () => {
}) })
}) })
describe('delete user', () => {
const now = new Date()
beforeEach(async () => {
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
})
it('marks the user as deleted', () => {
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('user_deleted')
})
})
describe('apollo returns error', () => { describe('apollo returns error', () => {
beforeEach(() => { beforeEach(() => {
apolloQueryMock.mockRejectedValue({ apolloQueryMock.mockRejectedValue({
@ -196,7 +211,7 @@ describe('UserSearch', () => {
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch') expect(toastErrorSpy).toBeCalledWith('Ouch')
}) })
}) })
}) })

View File

@ -94,11 +94,12 @@ export default {
this.searchResult = result.data.searchUsers.userList this.searchResult = result.data.searchUsers.userList
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.toastError(error.message)
}) })
}, },
updateDeletedAt(userId, deletedAt) { updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(this.$t('user_deleted'))
}, },
}, },
watch: { watch: {

View File

@ -5,11 +5,18 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// without this async calls are not working // without this async calls are not working
import 'regenerator-runtime' import 'regenerator-runtime'
import { toasters } from '../src/mixins/toaster'
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
global.localVue = createLocalVue() global.localVue = createLocalVue()
global.localVue.use(BootstrapVue) global.localVue.use(BootstrapVue)
global.localVue.use(IconsPlugin) global.localVue.use(IconsPlugin)
global.localVue.mixin(toasters)
// throw errors for vue warnings to force the programmers to take care about warnings // throw errors for vue warnings to force the programmers to take care about warnings
Vue.config.warnHandler = (w) => { Vue.config.warnHandler = (w) => {
throw new Error(w) throw new Error(w)

View File

@ -12512,11 +12512,6 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-toasted@^1.1.28:
version "1.1.28"
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
vue@^2.6.11: vue@^2.6.11:
version "2.6.14" version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"

View File

@ -17,6 +17,6 @@ export class Balance {
@Field(() => Decimal) @Field(() => Decimal)
decay: Decimal decay: Decimal
@Field(() => String) @Field(() => Date)
decayDate: string decayDate: Date
} }

View File

@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ObjectType, Field, Int } from 'type-graphql'
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
@ObjectType() @ObjectType()
@ -31,6 +29,6 @@ export class Decay {
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
end: Date | null end: Date | null
@Field(() => Number, { nullable: true }) @Field(() => Int, { nullable: true })
duration: number | null duration: number | null
} }

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
import { Decay } from './Decay' import { Decay } from './Decay'
import { Transaction as dbTransaction } from '@entity/Transaction' import { Transaction as dbTransaction } from '@entity/Transaction'
@ -25,7 +23,7 @@ export class Transaction {
transaction.decay, transaction.decay,
transaction.decayStart, transaction.decayStart,
transaction.balanceDate, transaction.balanceDate,
(transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000, Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000),
) )
} }
this.memo = transaction.memo this.memo = transaction.memo

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
import { KlickTipp } from './KlickTipp' import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User' import { User as dbUser } from '@entity/User'

View File

@ -307,13 +307,13 @@ export class AdminResolver {
const receivedCallDate = new Date() const receivedCallDate = new Date()
const transactionRepository = getCustomRepository(TransactionRepository) const transactionRepository = getCustomRepository(TransactionRepository)
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId) const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
if (lastUserTransaction) { if (lastTransaction) {
newBalance = calculateDecay( newBalance = calculateDecay(
lastUserTransaction.balance, lastTransaction.balance,
lastUserTransaction.balanceDate, lastTransaction.balanceDate,
receivedCallDate, receivedCallDate,
).balance ).balance
} }

View File

@ -30,7 +30,8 @@ import { RIGHTS } from '../../auth/RIGHTS'
import { User } from '../model/User' import { User } from '../model/User'
import { communityUser } from '../../util/communityUser' import { communityUser } from '../../util/communityUser'
import { virtualDecayTransaction } from '../../util/virtualDecayTransaction' import { virtualDecayTransaction } from '../../util/virtualDecayTransaction'
import Decimal from '../scalar/Decimal' import Decimal from 'decimal.js-light'
import { calculateDecay } from '../../util/decay'
@Resolver() @Resolver()
export class TransactionResolver { export class TransactionResolver {
@ -47,6 +48,7 @@ export class TransactionResolver {
}: Paginated, }: Paginated,
@Ctx() context: any, @Ctx() context: any,
): Promise<TransactionList> { ): Promise<TransactionList> {
const now = new Date()
// find user // find user
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
// TODO: separate those usecases - this is a security issue // TODO: separate those usecases - this is a security issue
@ -60,65 +62,6 @@ export class TransactionResolver {
{ order: { balanceDate: 'DESC' } }, { order: { balanceDate: 'DESC' } },
) )
if (!lastTransaction) {
// TODO Have proper return type here
throw new Error('User has no transactions')
}
// find transactions
const limit = currentPage === 1 && order === Order.DESC ? pageSize - 1 : pageSize
const offset =
currentPage === 1 ? 0 : (currentPage - 1) * pageSize - (order === Order.DESC ? 1 : 0)
const transactionRepository = getCustomRepository(TransactionRepository)
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
user.id,
limit,
offset,
order,
onlyCreations,
)
// find involved users
let involvedUserIds: number[] = []
userTransactions.forEach((transaction: dbTransaction) => {
involvedUserIds.push(transaction.userId)
if (transaction.linkedUserId) {
involvedUserIds.push(transaction.linkedUserId)
}
})
// remove duplicates
involvedUserIds = involvedUserIds.filter((value, index, self) => self.indexOf(value) === index)
// We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser
.createQueryBuilder()
.withDeleted()
.where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u))
const self = new User(user)
const transactions: Transaction[] = []
// decay transaction
if (currentPage === 1 && order === Order.DESC) {
const now = new Date()
transactions.push(
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
)
}
// transactions
for (let i = 0; i < userTransactions.length; i++) {
const userTransaction = userTransactions[i]
let linkedUser = null
if (userTransaction.typeId === TypeId.CREATION) {
linkedUser = communityUser
} else {
linkedUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
}
transactions.push(new Transaction(userTransaction, self, linkedUser))
}
// get GDT // get GDT
let balanceGDT = null let balanceGDT = null
try { try {
@ -134,9 +77,59 @@ export class TransactionResolver {
console.log('Could not query GDT Server', err) console.log('Could not query GDT Server', err)
} }
if (!lastTransaction) {
return new TransactionList(new Decimal(0), [], 0, balanceGDT)
}
// find transactions
// first page can contain 26 due to virtual decay transaction
const offset = (currentPage - 1) * pageSize
const transactionRepository = getCustomRepository(TransactionRepository)
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
user.id,
pageSize,
offset,
order,
onlyCreations,
)
// find involved users; I am involved
const involvedUserIds: number[] = [user.id]
userTransactions.forEach((transaction: dbTransaction) => {
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
involvedUserIds.push(transaction.linkedUserId)
}
})
// We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser
.createQueryBuilder()
.withDeleted()
.where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u))
const self = new User(user)
const transactions: Transaction[] = []
// decay transaction
if (currentPage === 1 && order === Order.DESC) {
transactions.push(
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
)
}
// transactions
userTransactions.forEach((userTransaction) => {
const linkedUser =
userTransaction.typeId === TransactionTypeId.CREATION
? communityUser
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
transactions.push(new Transaction(userTransaction, self, linkedUser))
})
// Construct Result // Construct Result
return new TransactionList( return new TransactionList(
lastTransaction.balance, calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
transactions, transactions,
userTransactionsCount, userTransactionsCount,
balanceGDT, balanceGDT,
@ -184,7 +177,7 @@ export class TransactionResolver {
transactionSend.memo = memo transactionSend.memo = memo
transactionSend.userId = senderUser.id transactionSend.userId = senderUser.id
transactionSend.linkedUserId = recipientUser.id transactionSend.linkedUserId = recipientUser.id
transactionSend.amount = amount transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay transactionSend.decay = sendBalance.decay.decay

View File

@ -7,36 +7,35 @@ describe('utils/decay', () => {
it('has base 0.99999997802044727', () => { it('has base 0.99999997802044727', () => {
const amount = new Decimal(1.0) const amount = new Decimal(1.0)
const seconds = 1 const seconds = 1
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727) // TODO: toString() was required, we could not compare two decimals
}) expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012')
// Not sure if the following skiped tests make sence!?
it('has negative decay?', async () => {
const amount = new Decimal(1.0)
const seconds = 1
expect(decayFormula(amount, seconds)).toBe(-0.99999997802044727)
}) })
it('has correct backward calculation', async () => { it('has correct backward calculation', async () => {
const amount = new Decimal(1.0) const amount = new Decimal(1.0)
const seconds = -1 const seconds = -1
expect(decayFormula(amount, seconds)).toBe(1.0000000219795533) expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444')
}) })
// not possible, nodejs hasn't enough accuracy // we get pretty close, but not exact here, skipping
it('has correct forward calculation', async () => { it.skip('has correct forward calculation', async () => {
const amount = new Decimal(1.0).div(0.99999997802044727) const amount = new Decimal(1.0).div(
new Decimal('0.99999997803504048973201202316767079413460520837376'),
)
const seconds = 1 const seconds = 1
expect(decayFormula(amount, seconds)).toBe(1.0) expect(decayFormula(amount, seconds).toString()).toBe('1.0')
}) })
}) })
it.skip('has base 0.99999997802044727', async () => { it('has base 0.99999997802044727', async () => {
const now = new Date() const now = new Date()
now.setSeconds(1) now.setSeconds(1)
const oneSecondAgo = new Date(now.getTime()) const oneSecondAgo = new Date(now.getTime())
oneSecondAgo.setSeconds(0) oneSecondAgo.setSeconds(0)
expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now)).toBe(0.99999997802044727) expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe(
'0.999999978035040489732012',
)
}) })
it('returns input amount when from and to is the same', async () => { it('returns input amount when from and to is the same', async () => {
const now = new Date() const now = new Date()
expect(calculateDecay(new Decimal(100.0), now, now).balance).toBe(100.0) expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100')
}) })
}) })

View File

@ -2,6 +2,7 @@ import Faker from 'faker'
import { define } from 'typeorm-seeding' import { define } from 'typeorm-seeding'
import { Transaction } from '../../entity/Transaction' import { Transaction } from '../../entity/Transaction'
import { TransactionContext } from '../interface/TransactionContext' import { TransactionContext } from '../interface/TransactionContext'
import Decimal from 'decimal.js-light'
define(Transaction, (faker: typeof Faker, context?: TransactionContext) => { define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
if (!context) { if (!context) {
@ -12,6 +13,8 @@ define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
transaction.typeId = context.typeId // || 2 transaction.typeId = context.typeId // || 2
transaction.userId = context.userId transaction.userId = context.userId
transaction.amount = context.amount transaction.amount = context.amount
transaction.balance = context.balance
transaction.decay = new Decimal(0) // context.decay
transaction.memo = context.memo transaction.memo = context.memo
transaction.creationDate = context.creationDate || new Date() transaction.creationDate = context.creationDate || new Date()
// transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null // transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null