Merge branch 'master' into 2285-mark-creation-via-link

This commit is contained in:
elweyn 2022-11-24 09:05:36 +01:00
commit b05c009d6e
35 changed files with 1810 additions and 243 deletions

View File

@ -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)
})
})
})

View File

@ -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>

View File

@ -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', () => {

View File

@ -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>

View 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)
})
})
})
})

View 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>

View File

@ -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",

View File

@ -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",

View 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',
)
})
})
})
})

View 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>

View File

@ -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)
}) })

View File

@ -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>

View File

@ -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()

View File

@ -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',

View File

@ -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

View File

@ -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",

View 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,
},
},
})
```

View 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')

View File

@ -0,0 +1 @@
= t('emails.accountMultiRegistration.subject')

View 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()
})
})
})

View 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
}

View 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'),
}),
})
})
})
})
})

View 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,
},
})
}

View File

@ -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',
}) })
}) })

View File

@ -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) {

View 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"
}
}
}

View 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"
}
}
}

View File

@ -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/'),
})
})
})

View File

@ -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,
}),
})
}

View File

@ -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...')
}) })

View File

@ -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 }
} }

View 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 }

View File

@ -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

View File

@ -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 }

File diff suppressed because it is too large Load Diff