Merge branch 'master' into 1880-no-throw-when-register-with-existing-email

This commit is contained in:
Wolfgang Huß 2022-06-21 11:11:56 +02:00 committed by GitHub
commit 4c420ecbf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 822 additions and 180 deletions

View File

@ -0,0 +1,254 @@
import { mount } from '@vue/test-utils'
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
import { setUserRole } from '../graphql/setUserRole'
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
setUserRole: null,
},
})
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$store: {
state: {
moderator: {
id: 0,
name: 'test moderator',
},
},
},
}
let propsData
let wrapper
describe('ChangeUserRoleFormular', () => {
const Wrapper = () => {
return mount(ChangeUserRoleFormular, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('DOM has', () => {
beforeEach(() => {
propsData = {
item: {
userId: 1,
isAdmin: null,
},
}
wrapper = Wrapper()
})
it('has a DIV element with the class.delete-user-formular', () => {
expect(wrapper.find('.change-user-role-formular').exists()).toBe(true)
})
})
describe('change own role', () => {
beforeEach(() => {
propsData = {
item: {
userId: 0,
isAdmin: null,
},
}
wrapper = Wrapper()
})
it('has the text that you cannot change own role', () => {
expect(wrapper.text()).toContain('userRole.notChangeYourSelf')
})
it('has role select disabled', () => {
expect(wrapper.find('select[disabled="disabled"]').exists()).toBe(true)
})
})
describe('change others role', () => {
let rolesToSelect
describe('general', () => {
beforeEach(() => {
propsData = {
item: {
userId: 1,
isAdmin: null,
},
}
wrapper = Wrapper()
rolesToSelect = wrapper.find('select.role-select').findAll('option')
})
it('has no text that you cannot change own role', () => {
expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf')
})
it('has the select label', () => {
expect(wrapper.text()).toContain('userRole.selectLabel')
})
it('has a select', () => {
expect(wrapper.find('select.role-select').exists()).toBe(true)
})
it('has role select enabled', () => {
expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false)
})
describe('on API error', () => {
beforeEach(() => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
rolesToSelect.at(1).setSelected()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
describe('user is usual user', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: new Date(),
},
})
propsData = {
item: {
userId: 1,
isAdmin: null,
},
}
wrapper = Wrapper()
rolesToSelect = wrapper.find('select.role-select').findAll('option')
})
it('has selected option set to "usual user"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('user')
})
describe('change select to', () => {
describe('same role', () => {
it('does not call the API', () => {
rolesToSelect.at(0).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role', () => {
beforeEach(() => {
rolesToSelect.at(1).setSelected()
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: true,
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: expect.any(Date),
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
})
})
describe('user is admin', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: null,
},
})
propsData = {
item: {
userId: 1,
isAdmin: new Date(),
},
}
wrapper = Wrapper()
rolesToSelect = wrapper.find('select.role-select').findAll('option')
})
it('has selected option set to "admin"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('admin')
})
describe('change select to', () => {
describe('same role', () => {
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role', () => {
beforeEach(() => {
rolesToSelect.at(0).setSelected()
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: false,
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: null,
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
})
})
})
})
})

View File

@ -0,0 +1,89 @@
<template>
<div class="change-user-role-formular">
<div class="shadow p-3 mb-5 bg-white rounded">
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
{{ $t('userRole.notChangeYourSelf') }}
</div>
<div class="m-3">
<label for="role" class="mr-3">{{ $t('userRole.selectLabel') }}</label>
<b-form-select
class="role-select"
v-model="roleSelected"
:options="roles"
:disabled="item.userId === $store.state.moderator.id"
/>
</div>
</div>
</div>
</template>
<script>
import { setUserRole } from '../graphql/setUserRole'
const rolesValues = {
admin: 'admin',
user: 'user',
}
export default {
name: 'ChangeUserRoleFormular',
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
roles: [
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
],
}
},
watch: {
roleSelected(newRole, oldRole) {
if (newRole !== oldRole) {
this.setUserRole(newRole, oldRole)
}
},
},
methods: {
setUserRole(newRole, oldRole) {
this.$apollo
.mutate({
mutation: setUserRole,
variables: {
userId: this.item.userId,
isAdmin: newRole === rolesValues.admin,
},
})
.then((result) => {
this.$emit('updateIsAdmin', {
userId: this.item.userId,
isAdmin: result.data.setUserRole,
})
this.toastSuccess(
this.$t('userRole.successfullyChangedTo', {
role:
result.data.setUserRole !== null
? this.$t('userRole.selectRoles.admin')
: this.$t('userRole.selectRoles.user'),
}),
)
})
.catch((error) => {
this.roleSelected = oldRole
this.toastError(error.message)
})
},
},
}
</script>
<style>
.role-select {
width: 300pt;
}
</style>

View File

@ -47,200 +47,200 @@ describe('DeletedUserFormular', () => {
}) })
it('has a DIV element with the class.delete-user-formular', () => { it('has a DIV element with the class.delete-user-formular', () => {
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy() expect(wrapper.find('.deleted-user-formular').exists()).toBe(true)
})
})
describe('delete self', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 0,
},
})
}) })
it('shows a text that you cannot delete yourself', () => { describe('delete self', () => {
expect(wrapper.text()).toBe('removeNotSelf') beforeEach(() => {
}) wrapper.setProps({
}) item: {
userId: 0,
describe('delete other user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: null,
},
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
})
it('shows the text "delete_user"', () => {
expect(wrapper.text()).toBe('delete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked()
})
it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBeTruthy()
})
it('has the button text "delete_user"', () => {
expect(wrapper.find('button').text()).toBe('delete_user')
})
describe('confirm delete with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: deleteUser,
variables: {
userId: 1,
},
}),
)
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
deletedAt: date,
},
]),
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm delete with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBeFalsy()
})
})
})
})
describe('recover user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: date,
},
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
})
it('shows the text "undelete_user"', () => {
expect(wrapper.text()).toBe('undelete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => {
apolloMutateMock.mockResolvedValue({
data: {
unDeleteUser: null,
}, },
}) })
await wrapper.find('input[type="checkbox"]').setChecked()
}) })
it('has a confirmation button', () => { it('shows a text that you cannot delete yourself', () => {
expect(wrapper.find('button').exists()).toBeTruthy() expect(wrapper.text()).toBe('removeNotSelf')
})
})
describe('delete other user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: null,
},
})
}) })
it('has the button text "undelete_user"', () => { it('has a checkbox', () => {
expect(wrapper.find('button').text()).toBe('undelete_user') expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
}) })
describe('confirm recover with success', () => { it('shows the text "delete_user"', () => {
expect(wrapper.text()).toBe('delete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.find('button').trigger('click') await wrapper.find('input[type="checkbox"]').setChecked()
}) })
it('calls the API', () => { it('has a confirmation button', () => {
expect(apolloMutateMock).toBeCalledWith( expect(wrapper.find('button').exists()).toBe(true)
expect.objectContaining({
mutation: unDeleteUser,
variables: {
userId: 1,
},
}),
)
}) })
it('emits update deleted At', () => { it('has the button text "delete_user"', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual( expect(wrapper.find('button').text()).toBe('delete_user')
expect.arrayContaining([ })
expect.arrayContaining([
{ describe('confirm delete with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: deleteUser,
variables: {
userId: 1, userId: 1,
deletedAt: null,
}, },
}),
)
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
deletedAt: date,
},
]),
]), ]),
]), )
) })
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
}) })
it('unchecks the checkbox', () => { describe('confirm delete with error', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined) beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBe(false)
})
})
})
})
describe('recover user', () => {
beforeEach(() => {
wrapper.setProps({
item: {
userId: 1,
deletedAt: date,
},
}) })
}) })
describe('confirm recover with error', () => { it('has a checkbox', () => {
beforeEach(async () => { expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
}) })
describe('click on checkbox again', () => { it('shows the text "undelete_user"', () => {
expect(wrapper.text()).toBe('undelete_user')
})
describe('click on checkbox', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false) apolloMutateMock.mockResolvedValue({
data: {
unDeleteUser: null,
},
})
await wrapper.find('input[type="checkbox"]').setChecked()
}) })
it('has no confirmation button anymore', () => { it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBeFalsy() expect(wrapper.find('button').exists()).toBe(true)
})
it('has the button text "undelete_user"', () => {
expect(wrapper.find('button').text()).toBe('undelete_user')
})
describe('confirm recover with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: unDeleteUser,
variables: {
userId: 1,
},
}),
)
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
deletedAt: null,
},
]),
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm recover with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBe(false)
})
}) })
}) })
}) })

View File

@ -28,6 +28,7 @@ export default {
props: { props: {
item: { item: {
type: Object, type: Object,
required: true,
}, },
}, },
data() { data() {

View File

@ -1,8 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import SearchUserTable from './SearchUserTable.vue' import SearchUserTable from './SearchUserTable.vue'
const date = new Date()
const localVue = global.localVue const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({}) const apolloMutateMock = jest.fn().mockResolvedValue({})
@ -96,16 +94,29 @@ describe('SearchUserTable', () => {
await wrapper.findAll('tbody > tr').at(1).trigger('click') await wrapper.findAll('tbody > tr').at(1).trigger('click')
}) })
describe('isAdmin', () => {
beforeEach(async () => {
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
userId: 1,
isAdmin: new Date(),
})
})
it('emits updateIsAdmin', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
})
})
describe('deleted at', () => { describe('deleted at', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', { await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
userId: 1, userId: 1,
deletedAt: date, deletedAt: new Date(),
}) })
}) })
it('emits updateDeletedAt', () => { it('emits updateDeletedAt', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]]) expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, expect.any(Date)]])
}) })
}) })

View File

@ -18,7 +18,7 @@
<template #cell(status)="row"> <template #cell(status)="row">
<div class="text-right"> <div class="text-right">
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light"> <b-avatar v-if="row.item.deletedAt" class="mr-3 test-deleted-icon" variant="light">
<b-iconstack font-scale="2"> <b-iconstack font-scale="2">
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon> <b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
<b-icon stacked icon="slash-circle" variant="danger"></b-icon> <b-icon stacked icon="slash-circle" variant="danger"></b-icon>
@ -79,6 +79,9 @@
<b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null"> <b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null">
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" /> <transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
</b-tab> </b-tab>
<b-tab :title="$t('userRole.tabTitle')">
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
</b-tab>
<b-tab :title="$t('delete_user')"> <b-tab :title="$t('delete_user')">
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" /> <deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
</b-tab> </b-tab>
@ -93,6 +96,7 @@ import CreationFormular from '../CreationFormular.vue'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue' import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
import CreationTransactionList from '../CreationTransactionList.vue' import CreationTransactionList from '../CreationTransactionList.vue'
import TransactionLinkList from '../TransactionLinkList.vue' import TransactionLinkList from '../TransactionLinkList.vue'
import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
import DeletedUserFormular from '../DeletedUserFormular.vue' import DeletedUserFormular from '../DeletedUserFormular.vue'
export default { export default {
@ -102,6 +106,7 @@ export default {
ConfirmRegisterMailFormular, ConfirmRegisterMailFormular,
CreationTransactionList, CreationTransactionList,
TransactionLinkList, TransactionLinkList,
ChangeUserRoleFormular,
DeletedUserFormular, DeletedUserFormular,
}, },
props: { props: {
@ -123,6 +128,9 @@ export default {
updateUserData(rowItem, newCreation) { updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation rowItem.creation = newCreation
}, },
updateIsAdmin({ userId, isAdmin }) {
this.$emit('updateIsAdmin', userId, isAdmin)
},
updateDeletedAt({ userId, deletedAt }) { updateDeletedAt({ userId, deletedAt }) {
this.$emit('updateDeletedAt', userId, deletedAt) this.$emit('updateDeletedAt', userId, deletedAt)
}, },

View File

@ -19,6 +19,7 @@ export const searchUsers = gql`
hasElopage hasElopage
emailConfirmationSend emailConfirmationSend
deletedAt deletedAt
isAdmin
} }
} }
} }

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const setUserRole = gql`
mutation ($userId: Int!, $isAdmin: Boolean!) {
setUserRole(userId: $userId, isAdmin: $isAdmin)
}
`

View File

@ -101,7 +101,7 @@
}, },
"redeemed": "eingelöst", "redeemed": "eingelöst",
"remove": "Entfernen", "remove": "Entfernen",
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.", "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen", "remove_all": "alle Nutzer entfernen",
"save": "Speichern", "save": "Speichern",
"status": "Status", "status": "Status",
@ -131,6 +131,16 @@
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.", "text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
"text_true": " Die Email wurde bestätigt." "text_true": " Die Email wurde bestätigt."
}, },
"userRole": {
"notChangeYourSelf": "Als Admin/Moderator kannst du nicht selber deine Rolle ändern.",
"selectLabel": "Rolle:",
"selectRoles": {
"admin": "Administrator",
"user": "einfacher Nutzer"
},
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
"tabTitle": "Nutzer-Rolle"
},
"user_deleted": "Nutzer ist gelöscht.", "user_deleted": "Nutzer ist gelöscht.",
"user_recovered": "Nutzer ist wiederhergestellt.", "user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche" "user_search": "Nutzer-Suche"

View File

@ -101,7 +101,7 @@
}, },
"redeemed": "redeemed", "redeemed": "redeemed",
"remove": "Remove", "remove": "Remove",
"removeNotSelf": "As admin / moderator you cannot delete yourself.", "removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users", "remove_all": "Remove all users",
"save": "Speichern", "save": "Speichern",
"status": "Status", "status": "Status",
@ -131,6 +131,16 @@
"text_false": "The last email was sent to the member ({email}) on {date}.", "text_false": "The last email was sent to the member ({email}) on {date}.",
"text_true": "The email was confirmed." "text_true": "The email was confirmed."
}, },
"userRole": {
"notChangeYourSelf": "As an admin/moderator, you cannot change your own role.",
"selectLabel": "Role:",
"selectRoles": {
"admin": "administrator",
"user": "usual user"
},
"successfullyChangedTo": "User is now \"{role}\".",
"tabTitle": "User Role"
},
"user_deleted": "User is deleted.", "user_deleted": "User is deleted.",
"user_recovered": "User is recovered.", "user_recovered": "User is recovered.",
"user_search": "User search" "user_search": "User search"

View File

@ -199,14 +199,43 @@ describe('UserSearch', () => {
}) })
}) })
describe('change user role', () => {
const userId = 4
describe('to admin', () => {
it('updates user role to admin', async () => {
await wrapper
.findComponent({ name: 'SearchUserTable' })
.vm.$emit('updateIsAdmin', userId, new Date())
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
expect.any(Date),
)
})
})
describe('to usual user', () => {
it('updates user role to usual user', async () => {
await wrapper
.findComponent({ name: 'SearchUserTable' })
.vm.$emit('updateIsAdmin', userId, null)
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
})
})
})
describe('delete user', () => { describe('delete user', () => {
const now = new Date() const userId = 4
beforeEach(async () => { beforeEach(() => {
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now) wrapper
.findComponent({ name: 'SearchUserTable' })
.vm.$emit('updateDeletedAt', userId, new Date())
}) })
it('marks the user as deleted', () => { it('marks the user as deleted', () => {
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now) expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).deletedAt).toEqual(
expect.any(Date),
)
expect(wrapper.find('.test-deleted-icon').exists()).toBe(true)
}) })
it('toasts a success message', () => { it('toasts a success message', () => {

View File

@ -42,6 +42,7 @@
type="PageUserSearch" type="PageUserSearch"
:items="searchResult" :items="searchResult"
:fields="fields" :fields="fields"
@updateIsAdmin="updateIsAdmin"
@updateDeletedAt="updateDeletedAt" @updateDeletedAt="updateDeletedAt"
/> />
<b-pagination <b-pagination
@ -111,6 +112,9 @@ export default {
this.toastError(error.message) this.toastError(error.message)
}) })
}, },
updateIsAdmin(userId, isAdmin) {
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
},
updateDeletedAt(userId, deletedAt) { updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered')) this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))

View File

@ -27,6 +27,9 @@ export enum RIGHTS {
GDT_BALANCE = 'GDT_BALANCE', GDT_BALANCE = 'GDT_BALANCE',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION', ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS', ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
@ -34,8 +37,6 @@ export enum RIGHTS {
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS', LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION', CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL', SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST', CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN', LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',

View File

@ -14,6 +14,7 @@ export class UserAdmin {
this.hasElopage = hasElopage this.hasElopage = hasElopage
this.deletedAt = user.deletedAt this.deletedAt = user.deletedAt
this.emailConfirmationSend = emailConfirmationSend this.emailConfirmationSend = emailConfirmationSend
this.isAdmin = user.isAdmin
} }
@Field(() => Number) @Field(() => Number)
@ -42,6 +43,9 @@ export class UserAdmin {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
emailConfirmationSend?: string emailConfirmationSend?: string
@Field(() => Date, { nullable: true })
isAdmin: Date | null
} }
@ObjectType() @ObjectType()

View File

@ -13,6 +13,7 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { import {
setUserRole,
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
adminCreateContribution, adminCreateContribution,
@ -69,6 +70,161 @@ let user: User
let creation: Contribution | void let creation: Contribution | void
describe('AdminResolver', () => { describe('AdminResolver', () => {
describe('set user role', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: 1, isAdmin: true } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id + 1, isAdmin: true } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('user to get a new role does not exist', () => {
it('throws an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
}),
)
})
})
describe('change role with success', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
})
describe('user gets new role', () => {
describe('to admin', () => {
it('returns date string', async () => {
const result = await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
})
expect(result).toEqual(
expect.objectContaining({
data: {
setUserRole: expect.any(String),
},
}),
)
expect(new Date(result.data.setUserRole)).toEqual(expect.any(Date))
})
})
describe('to usual user', () => {
it('returns null', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
).resolves.toEqual(
expect.objectContaining({
data: {
setUserRole: null,
},
}),
)
})
})
})
})
describe('change role with error', () => {
describe('is own role', () => {
it('throws an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Administrator can not change his own role!')],
}),
)
})
})
describe('user has already role to be set', () => {
describe('to admin', () => {
it('throws an error', async () => {
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
})
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User is already admin!')],
}),
)
})
})
describe('to usual user', () => {
it('throws an error', async () => {
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: false },
})
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User is already a usual user!')],
}),
)
})
})
})
})
})
})
})
describe('delete user', () => { describe('delete user', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {

View File

@ -73,7 +73,15 @@ export class AdminResolver {
} }
} }
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt'] const userFields = [
'id',
'firstName',
'lastName',
'email',
'emailChecked',
'deletedAt',
'isAdmin',
]
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered( const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
userFields.map((fieldName) => { userFields.map((fieldName) => {
return 'user.' + fieldName return 'user.' + fieldName
@ -133,6 +141,48 @@ export class AdminResolver {
} }
} }
@Authorized([RIGHTS.SET_USER_ROLE])
@Mutation(() => Date, { nullable: true })
async setUserRole(
@Arg('userId', () => Int)
userId: number,
@Arg('isAdmin', () => Boolean)
isAdmin: boolean,
@Ctx()
context: Context,
): Promise<Date | null> {
const user = await dbUser.findOne({ id: userId })
// user exists ?
if (!user) {
throw new Error(`Could not find user with userId: ${userId}`)
}
// administrator user changes own role?
const moderatorUser = getUser(context)
if (moderatorUser.id === userId) {
throw new Error('Administrator can not change his own role!')
}
// change isAdmin
switch (user.isAdmin) {
case null:
if (isAdmin === true) {
user.isAdmin = new Date()
} else {
throw new Error('User is already a usual user!')
}
break
default:
if (isAdmin === false) {
user.isAdmin = null
} else {
throw new Error('User is already admin!')
}
break
}
await user.save()
const newUser = await dbUser.findOne({ id: userId })
return newUser ? newUser.isAdmin : null
}
@Authorized([RIGHTS.DELETE_USER]) @Authorized([RIGHTS.DELETE_USER])
@Mutation(() => Date, { nullable: true }) @Mutation(() => Date, { nullable: true })
async deleteUser( async deleteUser(

View File

@ -98,6 +98,12 @@ export const confirmContribution = gql`
} }
` `
export const setUserRole = gql`
mutation ($userId: Int!, $isAdmin: Boolean!) {
setUserRole(userId: $userId, isAdmin: $isAdmin)
}
`
export const deleteUser = gql` export const deleteUser = gql`
mutation ($userId: Int!) { mutation ($userId: Int!) {
deleteUser(userId: $userId) deleteUser(userId: $userId)

View File

@ -110,6 +110,7 @@ export const searchUsers = gql`
hasElopage hasElopage
emailConfirmationSend emailConfirmationSend
deletedAt deletedAt
isAdmin
} }
} }
} }