Merge pull request #2984 from gradido/feat-username-in-wallet

feat(frontend): username in wallet
This commit is contained in:
Moriz Wahl 2023-05-17 11:21:22 +02:00 committed by GitHub
commit e9edf1a712
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 522 additions and 35 deletions

View File

@ -8,4 +8,5 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.SET_PASSWORD,
RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN,
RIGHTS.CHECK_USERNAME,
]

View File

@ -34,6 +34,7 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
CHECK_USERNAME = 'CHECK_USERNAME',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -53,6 +53,7 @@ import {
searchAdminUsers,
searchUsers,
user as userQuery,
checkUsername,
} from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
@ -2442,6 +2443,34 @@ describe('UserResolver', () => {
})
})
})
describe('check username', () => {
describe('reserved alias', () => {
it('returns false', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'root' } }),
).resolves.toMatchObject({
data: {
checkUsername: false,
},
errors: undefined,
})
})
})
describe('valid alias', () => {
it('returns true', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'valid' } }),
).resolves.toMatchObject({
data: {
checkUsername: true,
},
errors: undefined,
})
})
})
})
})
describe('printTimeDuration', () => {

View File

@ -498,6 +498,17 @@ export class UserResolver {
return true
}
@Authorized([RIGHTS.CHECK_USERNAME])
@Query(() => Boolean)
async checkUsername(@Arg('username') username: string): Promise<boolean> {
try {
await validateAlias(username)
return true
} catch {
return false
}
}
@Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean)
async updateUserInfos(

View File

@ -22,6 +22,12 @@ export const queryOptIn = gql`
}
`
export const checkUsername = gql`
query ($username: String!) {
checkUsername(username: $username)
}
`
export const transactionsQuery = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {

View File

@ -8,7 +8,7 @@
containsLowercaseCharacter: true,
containsUppercaseCharacter: true,
containsNumericCharacter: true,
atLeastEightCharactera: true,
atLeastEightCharacters: true,
atLeastOneSpecialCharater: true,
noWhitespaceCharacters: true,
}"

View File

@ -0,0 +1,71 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
vid="username"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label-for="labelFor">
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
autocomplete="off"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputUsername',
props: {
rules: {
default: () => {
return {
required: true,
}
},
},
name: { type: String, default: 'username' },
label: { type: String, default: 'Username' },
placeholder: { type: String, default: 'Username' },
value: { required: true, type: String },
showAllErrors: { type: Boolean, default: false },
immediate: { type: Boolean, default: false },
unique: { type: Boolean, required: true },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
},
}
</script>

View File

@ -0,0 +1,157 @@
import { mount } from '@vue/test-utils'
import UserName from './UserName'
import flushPromises from 'flush-promises'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPIcall = jest.fn()
const storeCommitMock = jest.fn()
describe('UserName Form', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
username: 'peter',
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
}
const Wrapper = () => {
return mount(UserName, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#username_form').exists()).toBeTruthy()
})
it('has an edit icon', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
it('renders the username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
})
describe('edit username', () => {
beforeEach(async () => {
await wrapper.find('svg.bi-pencil').trigger('click')
})
it('shows an cancel icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
})
it('closes the input when cancel icon is clicked', async () => {
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
})
it('does not change the username when cancel is clicked', async () => {
await wrapper.find('input').setValue('petra')
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
})
it('has a submit button', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
})
it('does not enable submit button when data is not changed', async () => {
await wrapper.find('form').trigger('keyup')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
describe('successfull submit', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('commits username to store', () => {
expect(storeCommitMock).toBeCalledWith('username', 'petra')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.username.change-success')
})
it('has an edit button again', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
})
describe('submit results in server error', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Error',
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Error')
})
})
})
describe('no username in store', () => {
beforeEach(() => {
mocks.$store.state.username = null
wrapper = Wrapper()
})
it('displays an information why to enter a username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('settings.username.no-username')
})
})
})
})

View File

@ -0,0 +1,130 @@
<template>
<b-card id="username_form" class="card-border-radius card-background-gray">
<div>
<b-row class="mb-4 text-right">
<b-col class="text-right">
<a
class="cursor-pointer"
@click="showUserData ? (showUserData = !showUserData) : cancelEdit()"
>
<span class="pointer mr-3">{{ $t('settings.username.change-username') }}</span>
<b-icon v-if="showUserData" class="pointer ml-3" icon="pencil"></b-icon>
<b-icon v-else icon="x-circle" class="pointer ml-3" variant="danger"></b-icon>
</a>
</b-col>
</b-row>
</div>
<div>
<validation-observer ref="usernameObserver" v-slot="{ handleSubmit, invalid }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-3">
<b-col class="col-12">
<small>
<b>{{ $t('form.username') }}</b>
</small>
</b-col>
<b-col v-if="showUserData" class="col-12">
<span v-if="username">
{{ username }}
</span>
<div v-else class="alert">
{{ $t('settings.username.no-username') }}
</div>
</b-col>
<b-col v-else class="col-12">
<input-username
v-model="username"
:name="$t('form.username')"
:placeholder="$t('form.username-placeholder')"
:showAllErrors="true"
:unique="true"
:rules="rules"
/>
</b-col>
</b-row>
<b-row class="text-right" v-if="!showUserData">
<b-col>
<div class="text-right" ref="submitButton">
<b-button
:variant="disabled(invalid) ? 'light' : 'success'"
@click="onSubmit"
type="submit"
:disabled="disabled(invalid)"
>
{{ $t('form.save') }}
</b-button>
</div>
</b-col>
</b-row>
</b-form>
</validation-observer>
</div>
</b-card>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
import InputUsername from '@/components/Inputs/InputUsername'
export default {
name: 'UserName',
components: {
InputUsername,
},
data() {
return {
showUserData: true,
username: this.$store.state.username || '',
usernameUnique: false,
rules: {
required: true,
min: 3,
max: 20,
usernameAllowedChars: true,
usernameHyphens: true,
usernameUnique: true,
},
}
},
methods: {
cancelEdit() {
this.username = this.$store.state.username || ''
this.showUserData = true
},
async onSubmit(event) {
event.preventDefault()
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
alias: this.username,
},
})
.then(() => {
this.$store.commit('username', this.username)
this.showUserData = true
this.toastSuccess(this.$t('settings.username.change-success'))
})
.catch((error) => {
this.toastError(error.message)
})
},
disabled(invalid) {
return !this.newUsername || invalid
},
},
computed: {
newUsername() {
return this.username !== this.$store.state.username
},
},
}
</script>
<style>
.cursor-pointer {
cursor: pointer;
}
div.alert {
color: red;
}
</style>

View File

@ -26,6 +26,7 @@ export const forgotPassword = gql`
export const updateUserInfos = gql`
mutation(
$alias: String
$firstName: String
$lastName: String
$password: String
@ -35,6 +36,7 @@ export const updateUserInfos = gql`
$hideAmountGDT: Boolean
) {
updateUserInfos(
alias: $alias
firstName: $firstName
lastName: $lastName
password: $password
@ -145,6 +147,7 @@ export const login = gql`
mutation($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
gradidoID
alias
firstName
lastName
language

View File

@ -89,6 +89,12 @@ export const queryOptIn = gql`
}
`
export const checkUsername = gql`
query($username: String!) {
checkUsername(username: $username)
}
`
export const queryTransactionLink = gql`
query($code: String!) {
queryTransactionLink(code: $code) {

View File

@ -167,12 +167,15 @@
"thx": "Danke",
"to": "bis",
"to1": "an",
"username": "Nutzername",
"username-placeholder": "Gebe einen eindeutigen Nutzernamen ein",
"validation": {
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
"usernmae-regex": "Der Username muss mit einem Buchstaben beginnen, auf den mindestens zwei alpha-numerische Zeichen folgen müssen.",
"usernmae-unique": "Der Username ist bereits vergeben."
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben."
},
"your_amount": "Dein Betrag"
},
@ -320,7 +323,12 @@
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
},
"showAmountGDD": "Dein GDD Betrag ist sichtbar.",
"showAmountGDT": "Dein GDT Betrag ist sichtbar."
"showAmountGDT": "Dein GDT Betrag ist sichtbar.",
"username": {
"change-success": "Dein Nutzername wurde erfolgreich geändert.",
"change-username": "Nutzername ändern",
"no-username": "Bitte gebe einen Nutzernamen ein. Damit hilfst du anderen Benutzern dich zu finden, ohne deine Email preisgeben zu müssen."
}
},
"signin": "Anmelden",
"signup": "Registrieren",

View File

@ -167,12 +167,15 @@
"thx": "Thank you",
"to": "to",
"to1": "to",
"username": "Username",
"username-placeholder": "Enter a unique username",
"validation": {
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
"is-not": "You cannot send Gradidos to yourself",
"usernmae-regex": "The username must start with a letter, followed by at least two alphanumeric characters.",
"usernmae-unique": "This username is already taken."
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken."
},
"your_amount": "Your amount"
},
@ -320,7 +323,12 @@
"subtitle": "If you have forgotten your password, you can reset it here."
},
"showAmountGDD": "Your GDD amount is visible.",
"showAmountGDT": "Your GDT amount is visible."
"showAmountGDT": "Your GDT amount is visible.",
"username": {
"change-success": "Your username has been changed successfully.",
"change-username": "Change username",
"no-username": "Please enter a username. This helps other users to find you without exposing your email."
}
},
"signin": "Sign in",
"signup": "Sign up",

View File

@ -27,7 +27,7 @@ const filters = loadFilters(i18n)
Vue.filter('amount', filters.amount)
Vue.filter('GDD', filters.GDD)
loadAllRules(i18n)
loadAllRules(i18n, apolloProvider.defaultClient)
addNavigationGuards(router, store, apolloProvider.defaultClient)

View File

@ -3,6 +3,8 @@
<user-card :balance="balance" :transactionCount="transactionCount"></user-card>
<user-data />
<hr />
<user-name />
<hr />
<user-password />
<hr />
<user-language />
@ -13,6 +15,7 @@
<script>
import UserCard from '@/components/UserSettings/UserCard'
import UserData from '@/components/UserSettings/UserData'
import UserName from '@/components/UserSettings/UserName'
import UserPassword from '@/components/UserSettings/UserPassword'
import UserLanguage from '@/components/UserSettings/UserLanguage'
import UserNewsletter from '@/components/UserSettings/UserNewsletter'
@ -22,6 +25,7 @@ export default {
components: {
UserCard,
UserData,
UserName,
UserPassword,
UserLanguage,
UserNewsletter,

View File

@ -16,9 +16,9 @@ export const mutations = {
gradidoID: (state, gradidoID) => {
state.gradidoID = gradidoID
},
// username: (state, username) => {
// state.username = username
// },
username: (state, username) => {
state.username = username
},
firstName: (state, firstName) => {
state.firstName = firstName
},
@ -59,7 +59,7 @@ export const actions = {
login: ({ dispatch, commit }, data) => {
commit('gradidoID', data.gradidoID)
commit('language', data.language)
// commit('username', data.username)
commit('username', data.alias)
commit('firstName', data.firstName)
commit('lastName', data.lastName)
commit('newsletterState', data.klickTipp.newsletterState)
@ -71,7 +71,7 @@ export const actions = {
},
logout: ({ commit, state }) => {
commit('token', null)
// commit('username', '')
commit('username', '')
commit('gradidoID', null)
commit('firstName', '')
commit('lastName', '')

View File

@ -26,6 +26,7 @@ const {
token,
firstName,
lastName,
username,
newsletterState,
publisherId,
isAdmin,
@ -104,6 +105,14 @@ describe('Vuex store', () => {
})
})
describe('username', () => {
it('sets the state of username', () => {
const state = { username: null }
username(state, 'peter')
expect(state.username).toEqual('peter')
})
})
describe('newsletterState', () => {
it('sets the state of newsletterState', () => {
const state = { newsletterState: null }
@ -166,6 +175,7 @@ describe('Vuex store', () => {
const commitedData = {
gradidoID: 'my-gradido-id',
language: 'de',
alias: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
klickTipp: {
@ -180,7 +190,7 @@ describe('Vuex store', () => {
it('calls eleven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(10)
expect(commit).toHaveBeenCalledTimes(11)
})
it('commits gradidoID', () => {
@ -193,44 +203,49 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(2, 'language', 'de')
})
it('commits username', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'username', 'peter')
})
it('commits firstName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'firstName', 'Peter')
expect(commit).toHaveBeenNthCalledWith(4, 'firstName', 'Peter')
})
it('commits lastName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'lastName', 'Lustig')
expect(commit).toHaveBeenNthCalledWith(5, 'lastName', 'Lustig')
})
it('commits newsletterState', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(5, 'newsletterState', true)
expect(commit).toHaveBeenNthCalledWith(6, 'newsletterState', true)
})
it('commits hasElopage', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(6, 'hasElopage', false)
expect(commit).toHaveBeenNthCalledWith(7, 'hasElopage', false)
})
it('commits publisherId', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'publisherId', 1234)
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', 1234)
})
it('commits isAdmin', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
expect(commit).toHaveBeenNthCalledWith(9, 'isAdmin', true)
})
it('commits hideAmountGDD', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
})
it('commits hideAmountGDT', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
})
})
@ -240,7 +255,7 @@ describe('Vuex store', () => {
it('calls eleven commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(10)
expect(commit).toHaveBeenCalledTimes(11)
})
it('commits token', () => {
@ -248,49 +263,54 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(1, 'token', null)
})
it('commits username', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(2, 'username', '')
})
it('commits gradidoID', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(2, 'gradidoID', null)
expect(commit).toHaveBeenNthCalledWith(3, 'gradidoID', null)
})
it('commits firstName', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(3, 'firstName', '')
expect(commit).toHaveBeenNthCalledWith(4, 'firstName', '')
})
it('commits lastName', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(4, 'lastName', '')
expect(commit).toHaveBeenNthCalledWith(5, 'lastName', '')
})
it('commits newsletterState', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(5, 'newsletterState', null)
expect(commit).toHaveBeenNthCalledWith(6, 'newsletterState', null)
})
it('commits hasElopage', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(6, 'hasElopage', false)
expect(commit).toHaveBeenNthCalledWith(7, 'hasElopage', false)
})
it('commits publisherId', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(7, 'publisherId', null)
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', null)
})
it('commits isAdmin', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
expect(commit).toHaveBeenNthCalledWith(9, 'isAdmin', false)
})
it('commits hideAmountGDD', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'hideAmountGDD', false)
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
})
it('commits hideAmountGDT', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDT', true)
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
})
// how to get this working?
it.skip('calls localStorage.clear()', () => {

View File

@ -1,8 +1,9 @@
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import { checkUsername } from '@/graphql/queries'
export const loadAllRules = (i18nCallback) => {
export const loadAllRules = (i18nCallback, apollo) => {
configure({
defaultMessage: (field, values) => {
// eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
@ -96,7 +97,7 @@ export const loadAllRules = (i18nCallback) => {
message: (_, values) => i18nCallback.t('site.signup.one_number', values),
})
extend('atLeastEightCharactera', {
extend('atLeastEightCharacters', {
validate(value) {
return !!value.match(/.{8,}/)
},
@ -123,4 +124,35 @@ export const loadAllRules = (i18nCallback) => {
},
message: (_, values) => i18nCallback.t('site.signup.dont_match', values),
})
extend('usernameAllowedChars', {
validate(value) {
return !!value.match(/^[a-zA-Z0-9_-]+$/)
},
message: (_, values) => i18nCallback.t('form.validation.username-allowed-chars', values),
})
extend('usernameHyphens', {
validate(value) {
return !!value.match(/^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/)
},
message: (_, values) => i18nCallback.t('form.validation.username-hyphens', values),
})
extend('usernameUnique', {
validate(value) {
if (!value.match(/^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/)) return true
return apollo
.query({
query: checkUsername,
variables: { username: value },
})
.then(({ data }) => {
return {
valid: data.checkUsername,
}
})
},
message: (_, values) => i18nCallback.t('form.validation.username-unique', values),
})
}

View File

@ -34,7 +34,7 @@ const i18nMock = {
n: (value, format) => value,
}
loadAllRules(i18nMock)
loadAllRules(i18nMock, { query: jest.fn().mockResolvedValue({ data: { checkUsername: true } }) })
global.localVue = createLocalVue()