mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2285-mark-creation-via-link
This commit is contained in:
commit
b05c009d6e
@ -1,39 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import CommunityStatistic from './CommunityStatistic'
|
|
||||||
|
|
||||||
const localVue = global.localVue
|
|
||||||
|
|
||||||
const mocks = {
|
|
||||||
$t: jest.fn((t) => t),
|
|
||||||
$n: jest.fn((n) => n),
|
|
||||||
}
|
|
||||||
|
|
||||||
const propsData = {
|
|
||||||
value: {
|
|
||||||
totalUsers: '123',
|
|
||||||
activeUsers: '100',
|
|
||||||
deletedUsers: '5',
|
|
||||||
totalGradidoCreated: '2500',
|
|
||||||
totalGradidoDecayed: '200',
|
|
||||||
totalGradidoAvailable: '500',
|
|
||||||
totalGradidoUnbookedDecayed: '111',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CommunityStatistic', () => {
|
|
||||||
let wrapper
|
|
||||||
|
|
||||||
const Wrapper = () => {
|
|
||||||
return mount(CommunityStatistic, { localVue, mocks, propsData })
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('mount', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = Wrapper()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders the Div Element ".community-statistic"', () => {
|
|
||||||
expect(wrapper.find('div.community-statistic').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="community-statistic">
|
|
||||||
<div>
|
|
||||||
<b-jumbotron bg-variant="info" text-variant="white" border-variant="dark">
|
|
||||||
<template #header>{{ $t('statistic.name') }}</template>
|
|
||||||
|
|
||||||
<hr class="my-4" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.totalUsers') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ value.totalUsers }}</b>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.activeUsers') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ value.activeUsers }}</b>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.deletedUsers') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ value.deletedUsers }}</b>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.totalGradidoCreated') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ $n(value.totalGradidoCreated, 'decimal') }} {{ $t('GDD') }}</b>
|
|
||||||
<small class="ml-5">{{ value.totalGradidoCreated }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.totalGradidoDecayed') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ $n(value.totalGradidoDecayed, 'decimal') }} {{ $t('GDD') }}</b>
|
|
||||||
<small class="ml-5">{{ value.totalGradidoDecayed }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.totalGradidoAvailable') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ $n(value.totalGradidoAvailable, 'decimal') }} {{ $t('GDD') }}</b>
|
|
||||||
<small class="ml-5">{{ value.totalGradidoAvailable }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ $t('statistic.totalGradidoUnbookedDecayed') }}{{ $t('math.colon') }}
|
|
||||||
<b>{{ $n(value.totalGradidoUnbookedDecayed, 'decimal') }} {{ $t('GDD') }}</b>
|
|
||||||
<small class="ml-5">{{ value.totalGradidoUnbookedDecayed }}</small>
|
|
||||||
</div>
|
|
||||||
</b-jumbotron>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CommunityStatistic',
|
|
||||||
props: {
|
|
||||||
value: { type: Object },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
CONFIG,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -49,28 +49,36 @@ describe('NavBar', () => {
|
|||||||
it('has a link to overview', () => {
|
it('has a link to overview', () => {
|
||||||
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
|
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a link to /user', () => {
|
it('has a link to /user', () => {
|
||||||
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user')
|
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a link to /creation', () => {
|
it('has a link to /creation', () => {
|
||||||
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
|
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a link to /creation-confirm', () => {
|
it('has a link to /creation-confirm', () => {
|
||||||
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe(
|
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe(
|
||||||
'/creation-confirm',
|
'/creation-confirm',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a link to /contribution-links', () => {
|
it('has a link to /contribution-links', () => {
|
||||||
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe(
|
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe(
|
||||||
'/contribution-links',
|
'/contribution-links',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('has a link to /statistic', () => {
|
||||||
|
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('/statistic')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('wallet', () => {
|
describe('wallet', () => {
|
||||||
const assignLocationSpy = jest.fn()
|
const assignLocationSpy = jest.fn()
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
|
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('changes window location to wallet', () => {
|
it.skip('changes window location to wallet', () => {
|
||||||
@ -89,7 +97,7 @@ describe('NavBar', () => {
|
|||||||
window.location = {
|
window.location = {
|
||||||
assign: windowLocationMock,
|
assign: windowLocationMock,
|
||||||
}
|
}
|
||||||
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click')
|
await wrapper.findAll('.nav-item').at(7).find('a').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('redirects to /logout', () => {
|
it('redirects to /logout', () => {
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
<b-nav-item to="/contribution-links">
|
<b-nav-item to="/contribution-links">
|
||||||
{{ $t('navbar.automaticContributions') }}
|
{{ $t('navbar.automaticContributions') }}
|
||||||
</b-nav-item>
|
</b-nav-item>
|
||||||
|
<b-nav-item to="/statistic">{{ $t('navbar.statistic') }}</b-nav-item>
|
||||||
<b-nav-item @click="wallet">{{ $t('navbar.my-account') }}</b-nav-item>
|
<b-nav-item @click="wallet">{{ $t('navbar.my-account') }}</b-nav-item>
|
||||||
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
|
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
|
||||||
</b-navbar-nav>
|
</b-navbar-nav>
|
||||||
|
|||||||
50
admin/src/components/Tables/StatisticTable.spec.js
Normal file
50
admin/src/components/Tables/StatisticTable.spec.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import StatisticTable from './StatisticTable.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
value: {
|
||||||
|
totalUsers: 3113,
|
||||||
|
activeUsers: 1057,
|
||||||
|
deletedUsers: 35,
|
||||||
|
totalGradidoCreated: '4083774.05000000000000000000',
|
||||||
|
totalGradidoDecayed: '-1062639.13634129622923372197',
|
||||||
|
totalGradidoAvailable: '2513565.869444365732411569',
|
||||||
|
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$n: jest.fn((n) => n),
|
||||||
|
$d: jest.fn((d) => d),
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('StatisticTable', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(StatisticTable, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV element with the class .statistic-table', () => {
|
||||||
|
expect(wrapper.find('div.statistic-table').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('renders the table', () => {
|
||||||
|
it('with three colunms', () => {
|
||||||
|
expect(wrapper.findAll('thead > tr > th')).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('with seven rows', () => {
|
||||||
|
expect(wrapper.findAll('tbody > tr')).toHaveLength(7)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
84
admin/src/components/Tables/StatisticTable.vue
Normal file
84
admin/src/components/Tables/StatisticTable.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!-- eslint-disable vue/no-static-inline-styles -->
|
||||||
|
<template>
|
||||||
|
<div class="statistic-table">
|
||||||
|
<b-table-simple style="width: auto" class="mt-5" striped stacked="md">
|
||||||
|
<b-thead>
|
||||||
|
<b-tr>
|
||||||
|
<b-th></b-th>
|
||||||
|
<b-th class="text-right">{{ $t('statistic.count') }}</b-th>
|
||||||
|
<b-th class="text-right">{{ $t('statistic.details') }}</b-th>
|
||||||
|
</b-tr>
|
||||||
|
</b-thead>
|
||||||
|
<b-tbody>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.totalUsers') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.totalUsers }}</b-td>
|
||||||
|
<b-td></b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.activeUsers') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.activeUsers }}</b-td>
|
||||||
|
<b-td></b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.deletedUsers') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.deletedUsers }}</b-td>
|
||||||
|
<b-td></b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.totalGradidoCreated') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">
|
||||||
|
{{ $n(value.totalGradidoCreated, 'decimal') }} {{ $t('GDD') }}
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.totalGradidoCreated }}</b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.totalGradidoDecayed') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">
|
||||||
|
{{ $n(value.totalGradidoDecayed, 'decimal') }} {{ $t('GDD') }}
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.totalGradidoDecayed }}</b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.totalGradidoAvailable') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">
|
||||||
|
{{ $n(value.totalGradidoAvailable, 'decimal') }} {{ $t('GDD') }}
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.totalGradidoAvailable }}</b-td>
|
||||||
|
</b-tr>
|
||||||
|
<b-tr>
|
||||||
|
<b-td>
|
||||||
|
<b>{{ $t('statistic.totalGradidoUnbookedDecayed') }}</b>
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">
|
||||||
|
{{ $n(value.totalGradidoUnbookedDecayed, 'decimal') }} {{ $t('GDD') }}
|
||||||
|
</b-td>
|
||||||
|
<b-td class="text-right">{{ value.totalGradidoUnbookedDecayed }}</b-td>
|
||||||
|
</b-tr>
|
||||||
|
</b-tbody>
|
||||||
|
</b-table-simple>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'StatisticTable',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -85,7 +85,6 @@
|
|||||||
"hide_details": "Details verbergen",
|
"hide_details": "Details verbergen",
|
||||||
"lastname": "Nachname",
|
"lastname": "Nachname",
|
||||||
"math": {
|
"math": {
|
||||||
"colon": ":",
|
|
||||||
"equals": "=",
|
"equals": "=",
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|",
|
"pipe": "|",
|
||||||
@ -104,6 +103,7 @@
|
|||||||
"my-account": "Mein Konto",
|
"my-account": "Mein Konto",
|
||||||
"open_creation": "Offene Schöpfungen",
|
"open_creation": "Offene Schöpfungen",
|
||||||
"overview": "Übersicht",
|
"overview": "Übersicht",
|
||||||
|
"statistic": "Statistik",
|
||||||
"user_search": "Nutzersuche"
|
"user_search": "Nutzersuche"
|
||||||
},
|
},
|
||||||
"not_open_creations": "Keine offenen Schöpfungen",
|
"not_open_creations": "Keine offenen Schöpfungen",
|
||||||
@ -125,8 +125,9 @@
|
|||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"statistic": {
|
"statistic": {
|
||||||
"activeUsers": "Aktive Mitglieder",
|
"activeUsers": "Aktive Mitglieder",
|
||||||
|
"count": "Menge",
|
||||||
"deletedUsers": "Gelöschte Mitglieder",
|
"deletedUsers": "Gelöschte Mitglieder",
|
||||||
"name": "Statistik",
|
"details": "Details",
|
||||||
"totalGradidoAvailable": "GDD insgesamt im Umlauf",
|
"totalGradidoAvailable": "GDD insgesamt im Umlauf",
|
||||||
"totalGradidoCreated": "GDD insgesamt geschöpft",
|
"totalGradidoCreated": "GDD insgesamt geschöpft",
|
||||||
"totalGradidoDecayed": "GDD insgesamt verfallen",
|
"totalGradidoDecayed": "GDD insgesamt verfallen",
|
||||||
|
|||||||
@ -85,7 +85,6 @@
|
|||||||
"hide_details": "Hide details",
|
"hide_details": "Hide details",
|
||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"math": {
|
"math": {
|
||||||
"colon": ":",
|
|
||||||
"equals": "=",
|
"equals": "=",
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|",
|
"pipe": "|",
|
||||||
@ -104,6 +103,7 @@
|
|||||||
"my-account": "My Account",
|
"my-account": "My Account",
|
||||||
"open_creation": "Open creations",
|
"open_creation": "Open creations",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
|
"statistic": "Statistic",
|
||||||
"user_search": "User search"
|
"user_search": "User search"
|
||||||
},
|
},
|
||||||
"not_open_creations": "No open creations",
|
"not_open_creations": "No open creations",
|
||||||
@ -125,8 +125,9 @@
|
|||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"statistic": {
|
"statistic": {
|
||||||
"activeUsers": "Active members",
|
"activeUsers": "Active members",
|
||||||
|
"count": "Count",
|
||||||
"deletedUsers": "Deleted members",
|
"deletedUsers": "Deleted members",
|
||||||
"name": "Statistic",
|
"details": "Details",
|
||||||
"totalGradidoAvailable": "Total GDD in circulation",
|
"totalGradidoAvailable": "Total GDD in circulation",
|
||||||
"totalGradidoCreated": "Total created GDD",
|
"totalGradidoCreated": "Total created GDD",
|
||||||
"totalGradidoDecayed": "Total GDD decay",
|
"totalGradidoDecayed": "Total GDD decay",
|
||||||
|
|||||||
98
admin/src/pages/CommunityStatistic.spec.js
Normal file
98
admin/src/pages/CommunityStatistic.spec.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import CommunityStatistic from './CommunityStatistic.vue'
|
||||||
|
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
||||||
|
import { toastErrorSpy } from '../../test/testSetup'
|
||||||
|
import VueApollo from 'vue-apollo'
|
||||||
|
import { createMockClient } from 'mock-apollo-client'
|
||||||
|
|
||||||
|
const mockClient = createMockClient()
|
||||||
|
const apolloProvider = new VueApollo({
|
||||||
|
defaultClient: mockClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
localVue.use(VueApollo)
|
||||||
|
|
||||||
|
const defaultData = () => {
|
||||||
|
return {
|
||||||
|
communityStatistics: {
|
||||||
|
totalUsers: 3113,
|
||||||
|
activeUsers: 1057,
|
||||||
|
deletedUsers: 35,
|
||||||
|
totalGradidoCreated: '4083774.05000000000000000000',
|
||||||
|
totalGradidoDecayed: '-1062639.13634129622923372197',
|
||||||
|
totalGradidoAvailable: '2513565.869444365732411569',
|
||||||
|
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$n: jest.fn((n) => n),
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CommunityStatistic', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const communityStatisticsMock = jest.fn()
|
||||||
|
|
||||||
|
mockClient.setRequestHandler(
|
||||||
|
communityStatistics,
|
||||||
|
communityStatisticsMock
|
||||||
|
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||||
|
.mockResolvedValue({ data: defaultData() }),
|
||||||
|
)
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(CommunityStatistic, { localVue, mocks, apolloProvider })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".community-statistic"', () => {
|
||||||
|
expect(wrapper.find('div.community-statistic').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('server response for get statistics is an error', () => {
|
||||||
|
it('toast an error message', () => {
|
||||||
|
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('server response for getting statistics is success', () => {
|
||||||
|
it('renders the data correctly', () => {
|
||||||
|
expect(wrapper.findAll('tr').at(1).findAll('td').at(1).text()).toEqual('3113')
|
||||||
|
expect(wrapper.findAll('tr').at(2).findAll('td').at(1).text()).toEqual('1057')
|
||||||
|
expect(wrapper.findAll('tr').at(3).findAll('td').at(1).text()).toEqual('35')
|
||||||
|
expect(wrapper.findAll('tr').at(4).findAll('td').at(1).text()).toEqual(
|
||||||
|
'4083774.05000000000000000000 GDD',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(4).findAll('td').at(2).text()).toEqual(
|
||||||
|
'4083774.05000000000000000000',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(5).findAll('td').at(1).text()).toEqual(
|
||||||
|
'-1062639.13634129622923372197 GDD',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(5).findAll('td').at(2).text()).toEqual(
|
||||||
|
'-1062639.13634129622923372197',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(6).findAll('td').at(1).text()).toEqual(
|
||||||
|
'2513565.869444365732411569 GDD',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(6).findAll('td').at(2).text()).toEqual(
|
||||||
|
'2513565.869444365732411569',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(7).findAll('td').at(1).text()).toEqual(
|
||||||
|
'-500474.6738366222166261272 GDD',
|
||||||
|
)
|
||||||
|
expect(wrapper.findAll('tr').at(7).findAll('td').at(2).text()).toEqual(
|
||||||
|
'-500474.6738366222166261272',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
42
admin/src/pages/CommunityStatistic.vue
Normal file
42
admin/src/pages/CommunityStatistic.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="community-statistic">
|
||||||
|
<statistic-table v-model="statistics" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
||||||
|
import StatisticTable from '../components/Tables/StatisticTable.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommunityStatistic',
|
||||||
|
components: {
|
||||||
|
StatisticTable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statistics: {
|
||||||
|
totalUsers: null,
|
||||||
|
activeUsers: null,
|
||||||
|
deletedUsers: null,
|
||||||
|
totalGradidoCreated: null,
|
||||||
|
totalGradidoDecayed: null,
|
||||||
|
totalGradidoAvailable: null,
|
||||||
|
totalGradidoUnbookedDecayed: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
CommunityStatistics: {
|
||||||
|
query() {
|
||||||
|
return communityStatistics
|
||||||
|
},
|
||||||
|
update({ communityStatistics }) {
|
||||||
|
this.statistics = communityStatistics
|
||||||
|
},
|
||||||
|
error({ message }) {
|
||||||
|
this.toastError(message)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import Overview from './Overview.vue'
|
import Overview from './Overview.vue'
|
||||||
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
|
||||||
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
@ -22,19 +21,6 @@ const apolloQueryMock = jest
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
communityStatistics: {
|
|
||||||
totalUsers: 3113,
|
|
||||||
activeUsers: 1057,
|
|
||||||
deletedUsers: 35,
|
|
||||||
totalGradidoCreated: '4083774.05000000000000000000',
|
|
||||||
totalGradidoDecayed: '-1062639.13634129622923372197',
|
|
||||||
totalGradidoAvailable: '2513565.869444365732411569',
|
|
||||||
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mockResolvedValue({
|
.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
listUnconfirmedContributions: [
|
listUnconfirmedContributions: [
|
||||||
@ -88,14 +74,6 @@ describe('Overview', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls communityStatistics', () => {
|
|
||||||
expect(apolloQueryMock).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
query: communityStatistics,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('commits three pending creations to store', () => {
|
it('commits three pending creations to store', () => {
|
||||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
|
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -28,32 +28,13 @@
|
|||||||
</b-link>
|
</b-link>
|
||||||
</b-card-text>
|
</b-card-text>
|
||||||
</b-card>
|
</b-card>
|
||||||
<community-statistic class="mt-5" v-model="statistics" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { communityStatistics } from '@/graphql/communityStatistics.js'
|
|
||||||
import CommunityStatistic from '../components/CommunityStatistic.vue'
|
|
||||||
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
components: {
|
|
||||||
CommunityStatistic,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
statistics: {
|
|
||||||
totalUsers: null,
|
|
||||||
activeUsers: null,
|
|
||||||
deletedUsers: null,
|
|
||||||
totalGradidoCreated: null,
|
|
||||||
totalGradidoDecayed: null,
|
|
||||||
totalGradidoAvailable: null,
|
|
||||||
totalGradidoUnbookedDecayed: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getPendingCreations() {
|
getPendingCreations() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
@ -65,30 +46,9 @@ export default {
|
|||||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getCommunityStatistics() {
|
|
||||||
this.$apollo
|
|
||||||
.query({
|
|
||||||
query: communityStatistics,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
this.statistics.totalUsers = result.data.communityStatistics.totalUsers
|
|
||||||
this.statistics.activeUsers = result.data.communityStatistics.activeUsers
|
|
||||||
this.statistics.deletedUsers = result.data.communityStatistics.deletedUsers
|
|
||||||
this.statistics.totalGradidoCreated = result.data.communityStatistics.totalGradidoCreated
|
|
||||||
this.statistics.totalGradidoDecayed = result.data.communityStatistics.totalGradidoDecayed
|
|
||||||
this.statistics.totalGradidoAvailable =
|
|
||||||
result.data.communityStatistics.totalGradidoAvailable
|
|
||||||
this.statistics.totalGradidoUnbookedDecayed =
|
|
||||||
result.data.communityStatistics.totalGradidoUnbookedDecayed
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.toastError('communityStatistics has no result, use default data')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getPendingCreations()
|
this.getPendingCreations()
|
||||||
this.getCommunityStatistics()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -44,8 +44,8 @@ describe('router', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('routes', () => {
|
describe('routes', () => {
|
||||||
it('has seven routes defined', () => {
|
it('has nine routes defined', () => {
|
||||||
expect(routes).toHaveLength(8)
|
expect(routes).toHaveLength(9)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has "/overview" as default', async () => {
|
it('has "/overview" as default', async () => {
|
||||||
@ -82,12 +82,19 @@ describe('router', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('contribution-links', () => {
|
describe('contribution-links', () => {
|
||||||
it('loads the "ContributionLinks" component', async () => {
|
it('loads the "ContributionLinks" page', async () => {
|
||||||
const component = await routes.find((r) => r.path === '/contribution-links').component()
|
const component = await routes.find((r) => r.path === '/contribution-links').component()
|
||||||
expect(component.default.name).toBe('ContributionLinks')
|
expect(component.default.name).toBe('ContributionLinks')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('statistics', () => {
|
||||||
|
it('loads the "CommunityStatistic" page', async () => {
|
||||||
|
const component = await routes.find((r) => r.path === '/statistic').component()
|
||||||
|
expect(component.default.name).toBe('CommunityStatistic')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('not found page', () => {
|
describe('not found page', () => {
|
||||||
it('renders the "NotFound" component', async () => {
|
it('renders the "NotFound" component', async () => {
|
||||||
const component = await routes.find((r) => r.path === '*').component()
|
const component = await routes.find((r) => r.path === '*').component()
|
||||||
|
|||||||
@ -6,6 +6,10 @@ const routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('@/pages/Overview.vue'),
|
component: () => import('@/pages/Overview.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/statistic',
|
||||||
|
component: () => import('@/pages/CommunityStatistic.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// TODO: Implement a "You are logged out"-Page
|
// TODO: Implement a "You are logged out"-Page
|
||||||
path: '/logout',
|
path: '/logout',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
##################################################################################
|
##################################################################################
|
||||||
# BASE ###########################################################################
|
# BASE ###########################################################################
|
||||||
##################################################################################
|
##################################################################################
|
||||||
FROM node:12.19.0-alpine3.10 as base
|
FROM node:18.7.0-alpine3.16 as base
|
||||||
|
|
||||||
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyperswarm/dht": "^6.2.0",
|
"@hyperswarm/dht": "^6.2.0",
|
||||||
|
"@types/email-templates": "^10.0.1",
|
||||||
|
"@types/i18n": "^0.13.4",
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
@ -30,14 +32,17 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"email-templates": "^10.0.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graphql": "^15.5.1",
|
"graphql": "^15.5.1",
|
||||||
|
"i18n": "^0.15.1",
|
||||||
"jest": "^27.2.4",
|
"jest": "^27.2.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"log4js": "^6.4.6",
|
"log4js": "^6.4.6",
|
||||||
"mysql2": "^2.3.0",
|
"mysql2": "^2.3.0",
|
||||||
"nodemailer": "^6.6.5",
|
"nodemailer": "^6.6.5",
|
||||||
|
"pug": "^3.0.2",
|
||||||
"random-bigint": "^0.0.1",
|
"random-bigint": "^0.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sodium-native": "^3.3.0",
|
"sodium-native": "^3.3.0",
|
||||||
|
|||||||
50
backend/src/emails/README.md
Normal file
50
backend/src/emails/README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Using `forwardemail`–`email-templates` With `pug` Package
|
||||||
|
|
||||||
|
You'll find the GitHub repository of the `email-templates` package and the `pug` package here:
|
||||||
|
|
||||||
|
- [email-templates](https://github.com/forwardemail/email-templates)
|
||||||
|
- [pug](https://www.npmjs.com/package/pug)
|
||||||
|
|
||||||
|
## `pug` Documentation
|
||||||
|
|
||||||
|
The full `pug` documentation you'll find here:
|
||||||
|
|
||||||
|
- [pugjs.org](https://pugjs.org/)
|
||||||
|
|
||||||
|
### Caching Possibility
|
||||||
|
|
||||||
|
In case we are sending many emails in the future there is the possibility to cache the `pug` templates:
|
||||||
|
|
||||||
|
- [cache-pug-templates](https://github.com/ladjs/cache-pug-templates)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test your send emails you have different possibilities:
|
||||||
|
|
||||||
|
### In General
|
||||||
|
|
||||||
|
To send emails to yourself while developing set in `.env` the value `EMAIL_TEST_MODUS=true` and `EMAIL_TEST_RECEIVER` to your preferred email address.
|
||||||
|
|
||||||
|
### Unit Or Integration Tests
|
||||||
|
|
||||||
|
To change the behavior to show previews etc. you have the following options to be set in `sendEmailTranslated.ts` on creating the email object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const email = new Email({
|
||||||
|
…
|
||||||
|
// send emails in development/test env:
|
||||||
|
send: true,
|
||||||
|
…
|
||||||
|
// to open send emails in the browser
|
||||||
|
preview: true,
|
||||||
|
// or
|
||||||
|
// to open send emails in a specific the browser
|
||||||
|
preview: {
|
||||||
|
open: {
|
||||||
|
app: 'firefox',
|
||||||
|
wait: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
…
|
||||||
|
})
|
||||||
|
```
|
||||||
22
backend/src/emails/accountMultiRegistration/html.pug
Normal file
22
backend/src/emails/accountMultiRegistration/html.pug
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang=locale)
|
||||||
|
head
|
||||||
|
title= t('emails.accountMultiRegistration.subject')
|
||||||
|
body
|
||||||
|
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
|
||||||
|
#container.col
|
||||||
|
p(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.helloName', { firstName, lastName })
|
||||||
|
p= t('emails.accountMultiRegistration.emailReused')
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.emailExists')
|
||||||
|
p= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
|
||||||
|
br
|
||||||
|
a(href=resendLink) #{resendLink}
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
|
||||||
|
p= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
|
||||||
|
br
|
||||||
|
a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/
|
||||||
|
p(style='margin-top: 24px;')= t('emails.accountMultiRegistration.sincerelyYours')
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.yourGradidoTeam')
|
||||||
1
backend/src/emails/accountMultiRegistration/subject.pug
Normal file
1
backend/src/emails/accountMultiRegistration/subject.pug
Normal file
@ -0,0 +1 @@
|
|||||||
|
= t('emails.accountMultiRegistration.subject')
|
||||||
110
backend/src/emails/sendEmailTranslated.test.ts
Normal file
110
backend/src/emails/sendEmailTranslated.test.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { createTransport } from 'nodemailer'
|
||||||
|
import { logger, i18n } from '@test/testSetup'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
CONFIG.EMAIL = false
|
||||||
|
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||||
|
CONFIG.EMAIL_SMTP_PORT = '1234'
|
||||||
|
CONFIG.EMAIL_USERNAME = 'user'
|
||||||
|
CONFIG.EMAIL_PASSWORD = 'pwd'
|
||||||
|
|
||||||
|
jest.mock('nodemailer', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
createTransport: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
sendMail: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
messageId: 'message',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendEmailTranslated', () => {
|
||||||
|
let result: Record<string, unknown> | null
|
||||||
|
|
||||||
|
describe('config email is false', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
result = await sendEmailTranslated({
|
||||||
|
receiver: {
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs warning', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false', () => {
|
||||||
|
expect(result).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('config email is true', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
CONFIG.EMAIL = true
|
||||||
|
result = await sendEmailTranslated({
|
||||||
|
receiver: {
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the transporter', () => {
|
||||||
|
expect(createTransport).toBeCalledWith({
|
||||||
|
host: 'EMAIL_SMTP_URL',
|
||||||
|
port: 1234,
|
||||||
|
secure: false,
|
||||||
|
requireTLS: true,
|
||||||
|
auth: {
|
||||||
|
user: 'user',
|
||||||
|
pass: 'pwd',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call of "sendEmailTranslated"', () => {
|
||||||
|
it('has expected result', () => {
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
envelope: {
|
||||||
|
from: 'info@gradido.net',
|
||||||
|
to: ['receiver@mail.org', 'support@gradido.net'],
|
||||||
|
},
|
||||||
|
message: expect.any(String),
|
||||||
|
originalMessage: expect.objectContaining({
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||||
|
attachments: [],
|
||||||
|
subject: 'Gradido: Try To Register Again With Your Email',
|
||||||
|
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
|
||||||
|
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls "i18n.setLocale" with "en"', () => {
|
||||||
|
expect(i18n.setLocale).toBeCalledWith('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls "i18n.__" for translation', () => {
|
||||||
|
expect(i18n.__).toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
85
backend/src/emails/sendEmailTranslated.ts
Normal file
85
backend/src/emails/sendEmailTranslated.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import path from 'path'
|
||||||
|
import { createTransport } from 'nodemailer'
|
||||||
|
import Email from 'email-templates'
|
||||||
|
import i18n from 'i18n'
|
||||||
|
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
export const sendEmailTranslated = async (params: {
|
||||||
|
receiver: {
|
||||||
|
to: string
|
||||||
|
cc?: string
|
||||||
|
}
|
||||||
|
template: string
|
||||||
|
locals: Record<string, string>
|
||||||
|
}): Promise<Record<string, unknown> | null> => {
|
||||||
|
let resultSend: Record<string, unknown> | null = null
|
||||||
|
|
||||||
|
// TODO: test the calling order of 'i18n.setLocale' for example: language of logging 'en', language of email receiver 'es', reset language of current user 'de'
|
||||||
|
|
||||||
|
// because language of receiver can differ from language of current user who triggers the sending
|
||||||
|
const rememberLocaleToRestore = i18n.getLocale()
|
||||||
|
|
||||||
|
i18n.setLocale('en') // for logging
|
||||||
|
logger.info(
|
||||||
|
`send Email: language=${params.locals.locale} to=${params.receiver.to}` +
|
||||||
|
(params.receiver.cc ? `, cc=${params.receiver.cc}` : '') +
|
||||||
|
`, subject=${i18n.__('emails.' + params.template + '.subject')}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!CONFIG.EMAIL) {
|
||||||
|
logger.info(`Emails are disabled via config...`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// because 'CONFIG.EMAIL_TEST_MODUS' can be boolean 'true' or string '`false`'
|
||||||
|
if (CONFIG.EMAIL_TEST_MODUS === true) {
|
||||||
|
logger.info(
|
||||||
|
`Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||||
|
)
|
||||||
|
params.receiver.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||||
|
}
|
||||||
|
const transport = createTransport({
|
||||||
|
host: CONFIG.EMAIL_SMTP_URL,
|
||||||
|
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||||
|
secure: false, // true for 465, false for other ports
|
||||||
|
requireTLS: true,
|
||||||
|
auth: {
|
||||||
|
user: CONFIG.EMAIL_USERNAME,
|
||||||
|
pass: CONFIG.EMAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
i18n.setLocale(params.locals.locale) // for email
|
||||||
|
|
||||||
|
// TESTING: see 'README.md'
|
||||||
|
const email = new Email({
|
||||||
|
message: {
|
||||||
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
|
},
|
||||||
|
transport,
|
||||||
|
preview: false,
|
||||||
|
// i18n, // is only needed if you don't install i18n
|
||||||
|
})
|
||||||
|
|
||||||
|
// ATTENTION: await is needed, because otherwise on send the email gets send in the language of the current user, because below the language gets reset
|
||||||
|
await email
|
||||||
|
.send({
|
||||||
|
template: path.join(__dirname, params.template),
|
||||||
|
message: params.receiver,
|
||||||
|
locals: params.locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
|
||||||
|
})
|
||||||
|
.then((result: Record<string, unknown>) => {
|
||||||
|
resultSend = result
|
||||||
|
logger.info('Send email successfully !!!')
|
||||||
|
logger.info('Result: ', result)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
logger.error('Error sending notification email: ', error)
|
||||||
|
throw new Error('Error sending notification email!')
|
||||||
|
})
|
||||||
|
|
||||||
|
i18n.setLocale(rememberLocaleToRestore)
|
||||||
|
|
||||||
|
return resultSend
|
||||||
|
}
|
||||||
88
backend/src/emails/sendEmailVariants.test.ts
Normal file
88
backend/src/emails/sendEmailVariants.test.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendAccountMultiRegistrationEmail } from './sendEmailVariants'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
CONFIG.EMAIL = true
|
||||||
|
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||||
|
CONFIG.EMAIL_SMTP_PORT = '1234'
|
||||||
|
CONFIG.EMAIL_USERNAME = 'user'
|
||||||
|
CONFIG.EMAIL_PASSWORD = 'pwd'
|
||||||
|
|
||||||
|
jest.mock('./sendEmailTranslated', () => {
|
||||||
|
const originalModule = jest.requireActual('./sendEmailTranslated')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendEmailTranslated: jest.fn((a) => originalModule.sendEmailTranslated(a)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendEmailVariants', () => {
|
||||||
|
let result: Record<string, unknown> | null
|
||||||
|
|
||||||
|
describe('sendAccountMultiRegistrationEmail', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await sendAccountMultiRegistrationEmail({
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
language: 'en',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls "sendEmailTranslated"', () => {
|
||||||
|
it('with expected parameters', () => {
|
||||||
|
expect(sendEmailTranslated).toBeCalledWith({
|
||||||
|
receiver: {
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
locale: 'en',
|
||||||
|
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has expected result', () => {
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
envelope: {
|
||||||
|
from: 'info@gradido.net',
|
||||||
|
to: ['peter@lustig.de'],
|
||||||
|
},
|
||||||
|
message: expect.any(String),
|
||||||
|
originalMessage: expect.objectContaining({
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||||
|
attachments: [],
|
||||||
|
subject: 'Gradido: Try To Register Again With Your Email',
|
||||||
|
html:
|
||||||
|
expect.stringContaining(
|
||||||
|
'<title>Gradido: Try To Register Again With Your Email</title>',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('>Gradido: Try To Register Again With Your Email</h1>') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Your email address has just been used again to register an account with Gradido.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'However, an account already exists for your email address.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Please click on the following link if you have forgotten your password:',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('or copy the link above into your browser window.') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'If you are not the one who tried to register again, please contact our support:',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Sincerely yours,<br><span>your Gradido team'),
|
||||||
|
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
20
backend/src/emails/sendEmailVariants.ts
Normal file
20
backend/src/emails/sendEmailVariants.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
export const sendAccountMultiRegistrationEmail = (data: {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
language: string
|
||||||
|
}): Promise<Record<string, unknown> | null> => {
|
||||||
|
return sendEmailTranslated({
|
||||||
|
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: data.language,
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ import { GraphQLError } from 'graphql'
|
|||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
@ -29,7 +29,7 @@ import { TransactionLink } from '@entity/TransactionLink'
|
|||||||
|
|
||||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||||
import { EventProtocol } from '@entity/EventProtocol'
|
import { EventProtocol } from '@entity/EventProtocol'
|
||||||
import { logger } from '@test/testSetup'
|
import { logger, i18n as localization } from '@test/testSetup'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
import { UserContact } from '@entity/UserContact'
|
import { UserContact } from '@entity/UserContact'
|
||||||
@ -46,7 +46,7 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
|
jest.mock('@/emails/sendEmailVariants', () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||||
@ -73,7 +73,7 @@ let mutate: any, query: any, con: any
|
|||||||
let testEnv: any
|
let testEnv: any
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
testEnv = await testEnvironment(logger)
|
testEnv = await testEnvironment(logger, localization)
|
||||||
mutate = testEnv.mutate
|
mutate = testEnv.mutate
|
||||||
query = testEnv.query
|
query = testEnv.query
|
||||||
con = testEnv.con
|
con = testEnv.con
|
||||||
@ -213,6 +213,7 @@ describe('UserResolver', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
language: 'de',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import i18n from 'i18n'
|
||||||
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
@ -18,7 +19,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
|
|||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
@ -358,6 +359,8 @@ export class UserResolver {
|
|||||||
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
|
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
|
||||||
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||||
|
|
||||||
|
i18n.setLocale(user.language)
|
||||||
|
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
logger.info('user.hasElopage=' + user.hasElopage)
|
logger.info('user.hasElopage=' + user.hasElopage)
|
||||||
@ -410,6 +413,7 @@ export class UserResolver {
|
|||||||
if (!language || !isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
i18n.setLocale(language)
|
||||||
|
|
||||||
// check if user with email still exists?
|
// check if user with email still exists?
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
@ -418,8 +422,11 @@ export class UserResolver {
|
|||||||
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||||
|
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
// ATTENTION: this logger-message will be exactly expected during tests
|
// ATTENTION: this logger-message will be exactly expected during tests, next line
|
||||||
logger.info(`User already exists with this email=${email}`)
|
logger.info(`User already exists with this email=${email}`)
|
||||||
|
logger.info(
|
||||||
|
`Specified username when trying to register multiple times with this email: firstName=${firstName}, lastName=${lastName}`,
|
||||||
|
)
|
||||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||||
|
|
||||||
const user = new User(communityDbUser)
|
const user = new User(communityDbUser)
|
||||||
@ -432,18 +439,20 @@ export class UserResolver {
|
|||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
logger.debug('partly faked user=' + user)
|
logger.debug('partly faked user=' + user)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||||
firstName,
|
firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside
|
||||||
lastName,
|
lastName: foundUser.lastName, // this is the real name of the email owner, but just "lastName" would be the name of the new registrant which shall not be passed to the outside
|
||||||
email,
|
email,
|
||||||
|
language: foundUser.language, // use language of the emails owner for sending
|
||||||
})
|
})
|
||||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
eventProtocol.writeEvent(
|
eventProtocol.writeEvent(
|
||||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||||
)
|
)
|
||||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
logger.info(
|
||||||
|
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
|
||||||
|
)
|
||||||
/* uncomment this, when you need the activation link on the console */
|
/* uncomment this, when you need the activation link on the console */
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
@ -787,6 +796,7 @@ export class UserResolver {
|
|||||||
throw new Error(`"${language}" isn't a valid language`)
|
throw new Error(`"${language}" isn't a valid language`)
|
||||||
}
|
}
|
||||||
userEntity.language = language
|
userEntity.language = language
|
||||||
|
i18n.setLocale(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password && passwordNew) {
|
if (password && passwordNew) {
|
||||||
|
|||||||
15
backend/src/locales/de.json
Normal file
15
backend/src/locales/de.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"emails": {
|
||||||
|
"accountMultiRegistration": {
|
||||||
|
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
|
||||||
|
"emailReused": "Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
|
||||||
|
"helloName": "Hallo {firstName} {lastName}",
|
||||||
|
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:",
|
||||||
|
"onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
|
||||||
|
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
|
||||||
|
"sincerelyYours": "Mit freundlichen Grüßen,",
|
||||||
|
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail",
|
||||||
|
"yourGradidoTeam": "dein Gradido-Team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
backend/src/locales/en.json
Normal file
15
backend/src/locales/en.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"emails": {
|
||||||
|
"accountMultiRegistration": {
|
||||||
|
"emailExists": "However, an account already exists for your email address.",
|
||||||
|
"emailReused": "Your email address has just been used again to register an account with Gradido.",
|
||||||
|
"helloName": "Hello {firstName} {lastName}",
|
||||||
|
"ifYouAreNotTheOne": "If you are not the one who tried to register again, please contact our support:",
|
||||||
|
"onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:",
|
||||||
|
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
|
||||||
|
"sincerelyYours": "Sincerely yours,",
|
||||||
|
"subject": "Gradido: Try To Register Again With Your Email",
|
||||||
|
"yourGradidoTeam": "your Gradido team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import CONFIG from '@/config'
|
|
||||||
import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
|
|
||||||
import { sendEMail } from './sendEMail'
|
|
||||||
|
|
||||||
jest.mock('./sendEMail', () => {
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
sendEMail: jest.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('sendAccountMultiRegistrationEmail', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await sendAccountMultiRegistrationEmail({
|
|
||||||
firstName: 'Peter',
|
|
||||||
lastName: 'Lustig',
|
|
||||||
email: 'peter@lustig.de',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls sendEMail', () => {
|
|
||||||
expect(sendEMail).toBeCalledWith({
|
|
||||||
to: `Peter Lustig <peter@lustig.de>`,
|
|
||||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
|
||||||
text:
|
|
||||||
expect.stringContaining('Hallo Peter Lustig') &&
|
|
||||||
expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
|
|
||||||
expect.stringContaining('https://gradido.net/de/contact/'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
|
||||||
import { accountMultiRegistration } from './text/accountMultiRegistration'
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
|
|
||||||
export const sendAccountMultiRegistrationEmail = (data: {
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
email: string
|
|
||||||
}): Promise<boolean> => {
|
|
||||||
return sendEMail({
|
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
|
||||||
subject: accountMultiRegistration.de.subject,
|
|
||||||
text: accountMultiRegistration.de.text({
|
|
||||||
...data,
|
|
||||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -38,7 +38,7 @@ describe('sendEMail', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs warining', () => {
|
it('logs warning', () => {
|
||||||
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,9 @@ import { Connection } from '@dbTools/typeorm'
|
|||||||
import { apolloLogger } from './logger'
|
import { apolloLogger } from './logger'
|
||||||
import { Logger } from 'log4js'
|
import { Logger } from 'log4js'
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
import { i18n } from './localization'
|
||||||
|
|
||||||
// TODO implement
|
// TODO implement
|
||||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||||
|
|
||||||
@ -34,6 +37,7 @@ const createServer = async (
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
context: any = serverContext,
|
context: any = serverContext,
|
||||||
logger: Logger = apolloLogger,
|
logger: Logger = apolloLogger,
|
||||||
|
localization: i18n.I18n = i18n,
|
||||||
): Promise<ServerDef> => {
|
): Promise<ServerDef> => {
|
||||||
logger.addContext('user', 'unknown')
|
logger.addContext('user', 'unknown')
|
||||||
logger.debug('createServer...')
|
logger.debug('createServer...')
|
||||||
@ -63,6 +67,9 @@ const createServer = async (
|
|||||||
// bodyparser urlencoded for elopage
|
// bodyparser urlencoded for elopage
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
app.use(localization.init)
|
||||||
|
|
||||||
// Elopage Webhook
|
// Elopage Webhook
|
||||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||||
|
|
||||||
@ -80,6 +87,7 @@ const createServer = async (
|
|||||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||||
)
|
)
|
||||||
logger.debug('createServer...successful')
|
logger.debug('createServer...successful')
|
||||||
|
|
||||||
return { apollo, app, con }
|
return { apollo, app, con }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
backend/src/server/localization.ts
Normal file
28
backend/src/server/localization.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { backendLogger } from './logger'
|
||||||
|
import i18n from 'i18n'
|
||||||
|
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['en', 'de'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
retryInDefaultLocale: false,
|
||||||
|
directory: path.join(__dirname, '..', 'locales'),
|
||||||
|
// autoReload: true, // if this is activated the seeding hangs at the very end
|
||||||
|
updateFiles: false,
|
||||||
|
objectNotation: true,
|
||||||
|
logDebugFn: (msg) => backendLogger.debug(msg),
|
||||||
|
logWarnFn: (msg) => backendLogger.info(msg),
|
||||||
|
logErrorFn: (msg) => backendLogger.error(msg),
|
||||||
|
// this api is needed for email-template pug files
|
||||||
|
api: {
|
||||||
|
__: 't', // now req.__ becomes req.t
|
||||||
|
__n: 'tn', // and req.__n can be called as req.tn
|
||||||
|
},
|
||||||
|
register: global,
|
||||||
|
mustacheConfig: {
|
||||||
|
tags: ['{', '}'],
|
||||||
|
disable: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { i18n }
|
||||||
@ -26,8 +26,8 @@ export const cleanDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testEnvironment = async (logger?: any) => {
|
export const testEnvironment = async (logger?: any, localization?: any) => {
|
||||||
const server = await createServer(context, logger)
|
const server = await createServer(context, logger, localization)
|
||||||
const con = server.con
|
const con = server.con
|
||||||
const testClient = createTestClient(server.apollo)
|
const testClient = createTestClient(server.apollo)
|
||||||
const mutate = testClient.mutate
|
const mutate = testClient.mutate
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { i18n } from '@/server/localization'
|
||||||
|
|
||||||
jest.setTimeout(1000000)
|
jest.setTimeout(1000000)
|
||||||
|
|
||||||
@ -19,4 +20,18 @@ jest.mock('@/server/logger', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export { logger }
|
jest.mock('@/server/localization', () => {
|
||||||
|
const originalModule = jest.requireActual('@/server/localization')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
i18n: {
|
||||||
|
init: jest.fn(),
|
||||||
|
// configure: jest.fn(),
|
||||||
|
// __: jest.fn(),
|
||||||
|
// setLocale: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { logger, i18n }
|
||||||
|
|||||||
1027
backend/yarn.lock
1027
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user