Merge pull request #701 from gradido/apollo-client

feat: Vue Apollo Client
This commit is contained in:
Moriz Wahl 2021-08-17 10:36:19 +02:00 committed by GitHub
commit b9b74f4073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1460 additions and 742 deletions

View File

@ -261,7 +261,7 @@ jobs:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 47 min_coverage: 59
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -1,16 +1,31 @@
import { ArgsType, Field } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType() @ArgsType()
export class GdtTransactionInput { export class GdtTransactionInput {
@Field(() => String) @Field(() => String)
email: string email: string
@Field(() => Number) @Field(() => Int, { nullable: true })
firstPage?: number currentPage?: number
@Field(() => Number) @Field(() => Int, { nullable: true })
items?: number pageSize?: number
@Field(() => String) @Field(() => String, { nullable: true })
order?: string
}
@ArgsType()
export class GdtTransactionSessionIdInput {
@Field(() => Number)
sessionId: number
@Field(() => Int, { nullable: true })
currentPage?: number
@Field(() => Int, { nullable: true })
pageSize?: number
@Field(() => String, { nullable: true })
order?: string order?: string
} }

View File

@ -9,6 +9,7 @@ export class Decay {
this.decayStart = json.decay_start this.decayStart = json.decay_start
this.decayEnd = json.decay_end this.decayEnd = json.decay_end
this.decayDuration = json.decay_duration this.decayDuration = json.decay_duration
this.decayStartBlock = json.decay_start_block
} }
@Field(() => Number) @Field(() => Number)
@ -22,4 +23,7 @@ export class Decay {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
decayDuration?: string decayDuration?: string
@Field(() => Int, { nullable: true })
decayStartBlock?: number
} }

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
export enum GdtEntryType { export enum GdtEntryType {

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { GdtEntry } from './GdtEntry' import { GdtEntry } from './GdtEntry'
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
@ -31,8 +33,8 @@ export class GdtEntryList {
@Field(() => Number) @Field(() => Number)
count: number count: number
@Field(() => [GdtEntry]) @Field(() => [GdtEntry], { nullable: true })
gdtEntries: GdtEntry[] gdtEntries?: GdtEntry[]
@Field(() => Number) @Field(() => Number)
gdtSum: number gdtSum: number

View File

@ -11,7 +11,6 @@ import { Decay } from './Decay'
@ObjectType() @ObjectType()
export class Transaction { export class Transaction {
constructor(json: any) { constructor(json: any) {
// console.log('Transaction constructor', json)
this.type = json.type this.type = json.type
this.balance = Number(json.balance) this.balance = Number(json.balance)
this.decayStart = json.decay_start this.decayStart = json.decay_start
@ -43,8 +42,8 @@ export class Transaction {
@Field(() => String) @Field(() => String)
memo: string memo: string
@Field(() => Number) @Field(() => Number, { nullable: true })
transactionId: number transactionId?: number
@Field({ nullable: true }) @Field({ nullable: true })
name?: string name?: string

View File

@ -2,7 +2,7 @@
import { Resolver, Query, /* Mutation, */ Args } from 'type-graphql' import { Resolver, Query, /* Mutation, */ Args } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { GdtEntryList } from '../models/GdtEntryList' import { GdtEntryList } from '../models/GdtEntryList'
import { GdtTransactionInput } from '../inputs/GdtInputs' import { GdtTransactionSessionIdInput } from '../inputs/GdtInputs'
import { apiGet } from '../../apis/loginAPI' import { apiGet } from '../../apis/loginAPI'
@Resolver() @Resolver()
@ -10,13 +10,12 @@ export class GdtResolver {
@Query(() => GdtEntryList) @Query(() => GdtEntryList)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async listGDTEntries( async listGDTEntries(
@Args() { email, firstPage = 1, items = 5, order = 'DESC' }: GdtTransactionInput, @Args()
{ currentPage = 1, pageSize = 5, order = 'DESC', sessionId }: GdtTransactionSessionIdInput,
): Promise<GdtEntryList> { ): Promise<GdtEntryList> {
email = email.trim().toLowerCase()
const result = await apiGet( const result = await apiGet(
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${email}/${firstPage}/${items}/${order}`, `${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${currentPage}/${pageSize}/${order}/${sessionId}`,
) )
if (!result.success) { if (!result.success) {
throw new Error(result.data) throw new Error(result.data)
} }

View File

@ -1,4 +1,4 @@
import { Resolver, Query, /* Mutation, */ Args } from 'type-graphql' import { Resolver, Query, Args } from 'type-graphql'
import CONFIG from '../../config' import CONFIG from '../../config'
import { TransactionList } from '../models/Transaction' import { TransactionList } from '../models/Transaction'
import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput' import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput'

View File

@ -1,3 +1,4 @@
LOGIN_API_URL=http://localhost/login_api/ LOGIN_API_URL=http://localhost/login_api/
COMMUNITY_API_URL=http://localhost/api/ COMMUNITY_API_URL=http://localhost/api/
ALLOW_REGISTER=true ALLOW_REGISTER=true
GRAPHQL_URI=http://localhost:4000/graphql

View File

@ -17,6 +17,7 @@
"@babel/preset-env": "^7.13.12", "@babel/preset-env": "^7.13.12",
"@vue/cli-plugin-unit-jest": "^4.5.12", "@vue/cli-plugin-unit-jest": "^4.5.12",
"@vue/test-utils": "^1.1.3", "@vue/test-utils": "^1.1.3",
"apollo-boost": "^0.4.9",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
@ -45,6 +46,7 @@
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"fuse.js": "^3.2.0", "fuse.js": "^3.2.0",
"google-maps": "^3.2.1", "google-maps": "^3.2.1",
"graphql": "^15.5.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-canvas-mock": "^2.3.1", "jest-canvas-mock": "^2.3.1",
@ -59,6 +61,7 @@
"sweetalert2": "^9.5.4", "sweetalert2": "^9.5.4",
"vee-validate": "^3.4.5", "vee-validate": "^3.4.5",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-apollo": "^3.0.7",
"vue-bootstrap-typeahead": "^0.2.6", "vue-bootstrap-typeahead": "^0.2.6",
"vue-chartjs": "^3.5.0", "vue-chartjs": "^3.5.0",
"vue-cli-plugin-i18n": "^1.0.1", "vue-cli-plugin-i18n": "^1.0.1",

View File

@ -1,71 +0,0 @@
import axios from 'axios'
import CONFIG from '../config'
const apiGet = async (url) => {
try {
const result = await axios.get(url)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
}
return { success: true, result }
} catch (error) {
return { success: false, result: error }
}
}
const apiPost = async (url, payload) => {
try {
const result = await axios.post(url, payload)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
}
return { success: true, result }
} catch (error) {
return { success: false, result: error }
}
}
const communityAPI = {
balance: async (sessionId) => {
return apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + sessionId)
},
transactions: async (sessionId, firstPage = 1, items = 5, order = 'DESC') => {
return apiGet(
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${sessionId}`,
)
},
transactionsgdt: async (sessionId, firstPage = 1, items = 5, order = 'DESC') => {
return apiGet(
`${CONFIG.COMMUNITY_API_URL}listGDTTransactions/${firstPage}/${items}/${order}/${sessionId}`,
)
},
/* http://localhost/vue/public/json-example/admin_transactionGdt_list.json
http://localhost/state-balances/ajaxGdtOverview
create: async (sessionId, email, amount, memo, target_date = new Date() ) => {
const payload = {
sessionId,
email,
amount,
target_date,
memo,
auto_sign: true,
}
return apiPost(CONFIG.COMMUNITY_API__URL + 'createCoins/', payload)
}, */
send: async (sessionId, data) => {
const payload = {
session_id: sessionId,
auto_sign: true,
...data,
}
return apiPost(CONFIG.COMMUNITY_API_URL + 'sendCoins/', payload)
},
}
export default communityAPI

View File

@ -1,145 +0,0 @@
import axios from 'axios'
import CONFIG from '../config'
// eslint-disable-next-line no-unused-vars
import regeneratorRuntime from 'regenerator-runtime'
// control email-text sended with email verification code
const EMAIL_TYPE = {
DEFAULT: 2, // if user has registered directly
ADMIN: 5, // if user was registered by an admin
}
const apiGet = async (url) => {
try {
const result = await axios.get(url)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (!['success', 'warning'].includes(result.data.state)) {
throw new Error(result.data.msg)
}
return { success: true, result }
} catch (error) {
return { success: false, result: error }
}
}
const apiPost = async (url, payload) => {
try {
const result = await axios.post(url, payload)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (result.data.state === 'warning') {
return { success: true, result: result.error }
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
}
return { success: true, result }
} catch (error) {
return { success: false, result: error }
}
}
const loginAPI = {
login: async (email, password) => {
const payload = {
email,
password,
}
return apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', payload)
},
logout: async (sessionId) => {
const payload = { session_id: sessionId }
return apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
},
create: async (email, firstName, lastName, password) => {
const payload = {
email,
first_name: firstName,
last_name: lastName,
password,
emailType: EMAIL_TYPE.DEFAULT,
login_after_register: true,
}
return apiPost(CONFIG.LOGIN_API_URL + 'createUser', payload)
},
sendEmail: async (email, email_text = 7, email_verification_code_type = 'resetPassword') => {
const payload = {
email,
email_text,
email_verification_code_type,
}
return apiPost(CONFIG.LOGIN_API_URL + 'sendEmail', payload)
},
loginViaEmailVerificationCode: async (optin) => {
return apiGet(
CONFIG.LOGIN_API_URL + 'loginViaEmailVerificationCode?emailVerificationCode=' + optin,
)
},
getUserInfos: async (sessionId, email) => {
const payload = {
session_id: sessionId,
email: email,
ask: ['user.first_name', 'user.last_name'],
}
return apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', payload)
},
updateUserInfos: async (sessionId, email, data) => {
const payload = {
session_id: sessionId,
email,
update: {
'User.first_name': data.firstName,
'User.last_name': data.lastName,
// 'User.description': data.description,
},
}
return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
},
changePassword: async (sessionId, email, password) => {
const payload = {
session_id: sessionId,
email,
password,
}
return apiPost(CONFIG.LOGIN_API_URL + 'resetPassword', payload)
},
changePasswordProfile: async (sessionId, email, password, passwordNew) => {
const payload = {
session_id: sessionId,
email,
update: {
'User.password_old': password,
'User.password': passwordNew,
},
}
return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
},
changeUsernameProfile: async (sessionId, email, username) => {
const payload = {
session_id: sessionId,
email,
update: {
'User.username': username,
},
}
return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
},
updateLanguage: async (sessionId, email, language) => {
const payload = {
session_id: sessionId,
email,
update: {
'User.language': language,
},
}
return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
},
checkUsername: async (username, groupId = 1) => {
return apiGet(CONFIG.LOGIN_API_URL + `checkUsername?username=${username}&group_id=${groupId}`)
},
}
export default loginAPI

View File

@ -7,7 +7,7 @@
<div v-if="decaytyp === 'new'"> <div v-if="decaytyp === 'new'">
<b-list-group style="border: 0px"> <b-list-group style="border: 0px">
<b-list-group-item style="border: 0px; background-color: #f1f1f1"> <b-list-group-item style="border: 0px; background-color: #f1f1f1">
<div class="d-flex" v-if="!decay.decay_start_block"> <div class="d-flex" v-if="!decay.decayStartBlock">
<div style="width: 100%" class="text-center pb-3"> <div style="width: 100%" class="text-center pb-3">
<b-icon icon="droplet-half" height="12" class="mb-2" /> <b-icon icon="droplet-half" height="12" class="mb-2" />
<b>{{ $t('decay.calculation_decay') }}</b> <b>{{ $t('decay.calculation_decay') }}</b>
@ -16,19 +16,19 @@
<div class="d-flex"> <div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2"> <div style="width: 40%" class="text-right pr-3 mr-2">
<div v-if="!decay.decay_start_block">{{ $t('decay.last_transaction') }}</div> <div v-if="!decay.decayStartBlock">{{ $t('decay.last_transaction') }}</div>
</div> </div>
<div style="width: 60%"> <div style="width: 60%">
<div v-if="decay.decay_start_block"> <div v-if="decay.decayStartBlock > 0">
<div class="display-4">{{ $t('decay.Starting_block_decay') }}</div> <div class="display-4">{{ $t('decay.Starting_block_decay') }}</div>
<div> <div>
{{ $t('decay.decay_introduced') }} : {{ $t('decay.decay_introduced') }} :
{{ $d($moment.unix(decay.decay_start), 'long') }} {{ $d($moment.unix(decay.decayStart), 'long') }}
</div> </div>
</div> </div>
<div> <div>
<span v-if="!decay.decay_start_block"> <span v-if="!decay.decayStart">
{{ $d($moment.unix(decay.decay_start), 'long') }} {{ $d($moment.unix(decay.decayStart), 'long') }}
{{ $i18n.locale === 'de' ? 'Uhr' : '' }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</span> </span>
</div> </div>
@ -37,10 +37,10 @@
<div class="d-flex"> <div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2"> <div style="width: 40%" class="text-right pr-3 mr-2">
<div v-if="!decay.decay_start_block">{{ $t('decay.past_time') }}</div> <div v-if="!decay.decayStartBlock">{{ $t('decay.past_time') }}</div>
</div> </div>
<div style="width: 60%"> <div style="width: 60%">
<div v-if="decay.decay_start_block">{{ $t('decay.since_introduction') }}</div> <div v-if="decay.decayStartBlock > 0">{{ $t('decay.since_introduction') }}</div>
<span v-if="duration"> <span v-if="duration">
<span v-if="duration.years > 0">{{ duration.years }} {{ $t('decay.year') }},</span> <span v-if="duration.years > 0">{{ duration.years }} {{ $t('decay.year') }},</span>
<span v-if="duration.months > 0"> <span v-if="duration.months > 0">
@ -68,24 +68,24 @@ export default {
props: { props: {
decay: { decay: {
balance: '', balance: '',
decay_duration: '', decayDuration: '',
decay_start: 0, decayStart: 0,
decay_end: 0, decayEnd: 0,
decay_start_block: 0, decayStartBlock: 0,
}, },
decaytyp: { type: String, default: '' }, decaytyp: { type: String, default: '' },
}, },
computed: { computed: {
decayStartBlockTextShort() { decayStartBlockTextShort() {
return this.decay.decay_start_block return this.decay.decayStartBlock
? this.$t('decay.decayStart') + this.$d(this.$moment.unix(this.decay.decay_start_block)) ? this.$t('decay.decayStart') + this.$d(this.$moment.unix(this.decay.decayStartBlock))
: '' : ''
}, },
duration() { duration() {
return this.$moment.duration( return this.$moment.duration(
this.$moment this.$moment
.unix(new Date(this.decay.decay_end)) .unix(new Date(this.decay.decayEnd))
.diff(this.$moment.unix(new Date(this.decay.decay_start))), .diff(this.$moment.unix(new Date(this.decay.decayStart))),
)._data )._data
}, },
}, },

View File

@ -3,6 +3,14 @@ import LanguageSwitch from './LanguageSwitch'
const localVue = global.localVue const localVue = global.localVue
const updateUserInfosQueryMock = jest.fn().mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
describe('LanguageSwitch', () => { describe('LanguageSwitch', () => {
let wrapper let wrapper
@ -20,6 +28,9 @@ describe('LanguageSwitch', () => {
$i18n: { $i18n: {
locale: 'en', locale: 'en',
}, },
$apollo: {
query: updateUserInfosQueryMock,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -37,17 +48,22 @@ describe('LanguageSwitch', () => {
describe('with locales en and de', () => { describe('with locales en and de', () => {
describe('empty store', () => { describe('empty store', () => {
it('shows English as default navigator langauge', () => { describe('navigator language is "en-US"', () => {
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en') const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as default navigator langauge', async () => {
languageGetter.mockReturnValue('en-US')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en')
})
}) })
describe('navigator language is "de-DE"', () => { describe('navigator language is "de-DE"', () => {
const mockNavigator = jest.fn(() => { const languageGetter = jest.spyOn(navigator, 'language', 'get')
return 'de'
})
it('shows Deutsch as language ', async () => { it('shows Deutsch as language ', async () => {
wrapper.vm.getNavigatorLanguage = mockNavigator languageGetter.mockReturnValue('de-DE')
wrapper.vm.setCurrentLanguage() wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Deutsch - de') expect(wrapper.find('button.dropdown-toggle').text()).toBe('Deutsch - de')
@ -55,12 +71,21 @@ describe('LanguageSwitch', () => {
}) })
describe('navigator language is "es-ES" (not supported)', () => { describe('navigator language is "es-ES" (not supported)', () => {
const mockNavigator = jest.fn(() => { const languageGetter = jest.spyOn(navigator, 'language', 'get')
return 'es'
})
it('shows English as language ', async () => { it('shows English as language ', async () => {
wrapper.vm.getNavigatorLanguage = mockNavigator languageGetter.mockReturnValue('es-ES')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en')
})
})
describe('no navigator langauge', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue(null)
wrapper.vm.setCurrentLanguage() wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en') expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en')
@ -91,5 +116,33 @@ describe('LanguageSwitch', () => {
}) })
}) })
}) })
describe('calls the API', () => {
it("with locale 'en'", () => {
wrapper.findAll('li').at(0).find('a').trigger('click')
expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1234,
email: 'he@ho.he',
locale: 'en',
},
}),
)
})
it("with locale 'de'", () => {
wrapper.findAll('li').at(1).find('a').trigger('click')
expect(updateUserInfosQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1234,
email: 'he@ho.he',
locale: 'de',
},
}),
)
})
})
}) })
}) })

View File

@ -14,7 +14,7 @@
<script> <script>
import { localeChanged } from 'vee-validate' import { localeChanged } from 'vee-validate'
import locales from '../locales/' import locales from '../locales/'
import loginAPI from '../apis/loginAPI' import { updateUserInfos } from '../graphql/queries'
export default { export default {
name: 'LanguageSwitch', name: 'LanguageSwitch',
@ -34,16 +34,21 @@ export default {
async saveLocale(locale) { async saveLocale(locale) {
this.setLocale(locale) this.setLocale(locale)
if (this.$store.state.sessionId && this.$store.state.email) { if (this.$store.state.sessionId && this.$store.state.email) {
const result = await loginAPI.updateLanguage( this.$apollo
this.$store.state.sessionId, .query({
this.$store.state.email, query: updateUserInfos,
locale, variables: {
) sessionId: this.$store.state.sessionId,
if (result.success) { email: this.$store.state.email,
// toast success message locale: locale,
} else { },
// toast error message })
} .then(() => {
// toast success message
})
.catch(() => {
// toast error message
})
} }
}, },
getLocaleObject(code) { getLocaleObject(code) {

View File

@ -13,6 +13,7 @@ const environment = {
const server = { const server = {
LOGIN_API_URL: process.env.LOGIN_API_URL || 'http://localhost/login_api/', LOGIN_API_URL: process.env.LOGIN_API_URL || 'http://localhost/login_api/',
COMMUNITY_API_URL: process.env.COMMUNITY_API_URL || 'http://localhost/api/', COMMUNITY_API_URL: process.env.COMMUNITY_API_URL || 'http://localhost/api/',
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
} }
const CONFIG = { const CONFIG = {

View File

@ -0,0 +1,142 @@
import gql from 'graphql-tag'
export const login = gql`
query($email: String!, $password: String!) {
login(email: $email, password: $password) {
sessionId
user {
email
firstName
lastName
language
username
description
}
}
}
`
export const logout = gql`
query($sessionId: Float!) {
logout(sessionId: $sessionId)
}
`
export const resetPassword = gql`
query($sessionId: Float!, $email: String!, $password: String!) {
resetPassword(sessionId: $sessionId, email: $email, password: $password)
}
`
export const loginViaEmailVerificationCode = gql`
query($optin: String!) {
loginViaEmailVerificationCode(optin: $optin) {
sessionId
email
}
}
`
export const updateUserInfos = gql`
query(
$sessionId: Float!
$email: String!
$firstName: String
$lastName: String
$description: String
$username: String
$password: String
$passwordNew: String
$locale: String
) {
updateUserInfos(
sessionId: $sessionId
email: $email
firstName: $firstName
lastName: $lastName
description: $description
username: $username
password: $password
passwordNew: $passwordNew
language: $locale
) {
validValues
}
}
`
export const transactionsQuery = gql`
query($sessionId: Float!, $firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") {
transactionList(sessionId: $sessionId, firstPage: $firstPage, items: $items, order: $order) {
gdtSum
count
balance
decay
decayDate
transactions {
type
balance
decayStart
decayEnd
decayDuration
memo
transactionId
name
email
date
decay {
balance
decayStart
decayEnd
decayDuration
decayStartBlock
}
}
}
}
`
export const resgisterUserQuery = gql`
query($firstName: String!, $lastName: String!, $email: String!, $password: String!) {
create(email: $email, firstName: $firstName, lastName: $lastName, password: $password)
}
`
export const sendCoins = gql`
query($sessionId: Float!, $email: String!, $amount: Float!, $memo: String!) {
sendCoins(sessionId: $sessionId, email: $email, amount: $amount, memo: $memo)
}
`
export const sendResetPasswordEmail = gql`
query($email: String!) {
sendResetPasswordEmail(email: $email) {
state
}
}
`
export const checkUsername = gql`
query($username: String!) {
checkUsername(username: $username) {
state
}
}
`
export const listGDTEntriesQuery = gql`
query($currentPage: Int!, $pageSize: Int!, $sessionId: Float!) {
listGDTEntries(currentPage: $currentPage, pageSize: $pageSize, sessionId: $sessionId) {
count
gdtEntries {
amount
date
comment
gdtEntryType
factor
gdt
}
gdtSum
}
}
`

View File

@ -3,11 +3,22 @@ import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue' import App from './App.vue'
import i18n from './i18n.js' import i18n from './i18n.js'
import { loadAllRules } from './validation-rules' import { loadAllRules } from './validation-rules'
import ApolloClient from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from './config'
import { store } from './store/store' import { store } from './store/store'
import router from './routes/router' import router from './routes/router'
const apolloClient = new ApolloClient({
uri: CONFIG.GRAPHQL_URI,
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
// plugin setup // plugin setup
Vue.use(DashboardPlugin) Vue.use(DashboardPlugin)
Vue.config.productionTip = false Vue.config.productionTip = false
@ -28,5 +39,6 @@ new Vue({
router, router,
store, store,
i18n, i18n,
apolloProvider,
render: (h) => h(App), render: (h) => h(App),
}) })

View File

@ -25,6 +25,8 @@ import VueMoment from 'vue-moment'
import Loading from 'vue-loading-overlay' import Loading from 'vue-loading-overlay'
import 'vue-loading-overlay/dist/vue-loading.css' import 'vue-loading-overlay/dist/vue-loading.css'
import VueApollo from 'vue-apollo'
export default { export default {
install(Vue) { install(Vue) {
Vue.use(GlobalComponents) Vue.use(GlobalComponents)
@ -38,6 +40,7 @@ export default {
Vue.use(VueQrcode) Vue.use(VueQrcode)
Vue.use(FlatPickr) Vue.use(FlatPickr)
Vue.use(Loading) Vue.use(Loading)
Vue.use(VueApollo)
Vue.use(Toasted, { Vue.use(Toasted, {
position: 'top-center', position: 'top-center',
duration: 5000, duration: 5000,

View File

@ -33,8 +33,8 @@ export const actions = {
commit('email', data.user.email) commit('email', data.user.email)
commit('language', data.user.language) commit('language', data.user.language)
commit('username', data.user.username) commit('username', data.user.username)
commit('firstName', data.user.first_name) commit('firstName', data.user.firstName)
commit('lastName', data.user.last_name) commit('lastName', data.user.lastName)
commit('description', data.user.description) commit('description', data.user.description)
}, },
logout: ({ commit, state }) => { logout: ({ commit, state }) => {

View File

@ -72,8 +72,8 @@ describe('Vuex store', () => {
email: 'someone@there.is', email: 'someone@there.is',
language: 'en', language: 'en',
username: 'user', username: 'user',
first_name: 'Peter', firstName: 'Peter',
last_name: 'Lustig', lastName: 'Lustig',
description: 'Nickelbrille', description: 'Nickelbrille',
}, },
} }

View File

@ -1,7 +1,7 @@
import { configure, extend } from 'vee-validate' import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules' import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import loginAPI from './apis/loginAPI' import { checkUsername } from './graphql/queries'
export const loadAllRules = (i18nCallback) => { export const loadAllRules = (i18nCallback) => {
configure({ configure({
@ -51,8 +51,19 @@ export const loadAllRules = (i18nCallback) => {
extend('gddUsernameUnique', { extend('gddUsernameUnique', {
async validate(value) { async validate(value) {
const result = await loginAPI.checkUsername(value) this.$apollo
return result.result.data.state === 'success' .query({
query: checkUsername,
variables: {
username: value,
},
})
.then((result) => {
return result.data.checkUsername.state === 'success'
})
.catch(() => {
return false
})
}, },
message: (_, values) => i18nCallback.t('form.validation.usernmae-unique', values), message: (_, values) => i18nCallback.t('form.validation.usernmae-unique', values),
}) })

View File

@ -1,5 +1,4 @@
import { mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import Vuex from 'vuex'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import DashboardLayoutGdd from './DashboardLayout_gdd' import DashboardLayoutGdd from './DashboardLayout_gdd'
@ -7,11 +6,15 @@ jest.useFakeTimers()
const localVue = global.localVue const localVue = global.localVue
const transitionStub = () => ({ const storeDispatchMock = jest.fn()
render(h) { const storeCommitMock = jest.fn()
return this.$options._renderChildren const routerPushMock = jest.fn()
const apolloMock = jest.fn().mockResolvedValue({
data: {
logout: 'success',
}, },
}) })
const toasterMock = jest.fn()
describe('DashboardLayoutGdd', () => { describe('DashboardLayoutGdd', () => {
let wrapper let wrapper
@ -28,34 +31,31 @@ describe('DashboardLayoutGdd', () => {
}, },
}, },
$router: { $router: {
push: jest.fn(), push: routerPushMock,
}, },
} $toasted: {
error: toasterMock,
const state = { },
user: { $apollo: {
name: 'Peter Lustig', query: apolloMock,
balance: 2546, },
balance_gdt: 20, $store: {
state: {
sessionId: 1,
email: 'user@example.org',
},
dispatch: storeDispatchMock,
commit: storeCommitMock,
}, },
email: 'peter.lustig@example.org',
} }
const stubs = { const stubs = {
RouterLink: RouterLinkStub, RouterLink: RouterLinkStub,
FadeTransition: transitionStub(), RouterView: true,
RouterView: transitionStub(),
} }
const store = new Vuex.Store({
state,
mutations: {
language: jest.fn(),
},
})
const Wrapper = () => { const Wrapper = () => {
return mount(DashboardLayoutGdd, { localVue, mocks, store, stubs }) return mount(DashboardLayoutGdd, { localVue, mocks, stubs })
} }
describe('mount', () => { describe('mount', () => {
@ -82,7 +82,7 @@ describe('DashboardLayoutGdd', () => {
navbar = wrapper.findAll('ul.navbar-nav').at(0) navbar = wrapper.findAll('ul.navbar-nav').at(0)
}) })
it('has five items in the navbar', () => { it('has three items in the navbar', () => {
expect(navbar.findAll('ul > a')).toHaveLength(3) expect(navbar.findAll('ul > a')).toHaveLength(3)
}) })
@ -99,54 +99,152 @@ describe('DashboardLayoutGdd', () => {
expect(navbar.findAll('ul > a').at(1).text()).toEqual('transactions') expect(navbar.findAll('ul > a').at(1).text()).toEqual('transactions')
}) })
// to do: get this working! it('has second item "transactions" linked to transactions in navbar', async () => {
it.skip('has second item "transactions" linked to transactions in navbar', async () => { expect(wrapper.findAll('a').at(3).attributes('href')).toBe('/transactions')
navbar.findAll('ul > a').at(1).trigger('click')
await flushPromises()
await jest.runAllTimers()
await flushPromises()
expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/transactions')
}) })
it('has tree items in the navbar', () => { it('has three items in the navbar', () => {
expect(navbar.findAll('ul > a')).toHaveLength(3) expect(navbar.findAll('ul > a')).toHaveLength(3)
}) })
it('has third item "My profile" in navbar', () => { it('has third item "My profile" linked to profile in navbar', async () => {
expect(navbar.findAll('ul > a').at(2).text()).toEqual('site.navbar.my-profil') expect(wrapper.findAll('a').at(5).attributes('href')).toBe('/profile')
}) })
it.skip('has third item "My profile" linked to profile in navbar', async () => { it('has a link to the members area', () => {
navbar.findAll('ul > a').at(2).trigger('click') expect(wrapper.findAll('ul').at(2).text()).toBe('members_area')
await flushPromises() expect(wrapper.findAll('ul').at(2).find('a').attributes('href')).toBe(
await jest.runAllTimers() 'https://elopage.com/s/gradido/sign_in?locale=en',
await flushPromises() )
expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profile')
}) })
// it('has fourth item "Settigs" in navbar', () => { it('has a locale switch', () => {
// expect(navbar.findAll('ul > li').at(3).text()).toEqual('site.navbar.settings') expect(wrapper.find('div.language-switch').exists()).toBeTruthy()
// }) })
//
// it.skip('has fourth item "Settings" linked to profileedit in navbar', async () => {
// navbar.findAll('ul > li > a').at(3).trigger('click')
// await flushPromises()
// await jest.runAllTimers()
// await flushPromises()
// expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profileedit')
// })
// it('has fifth item "Activity" in navbar', () => { it('has a logout button', () => {
// expect(navbar.findAll('ul > li').at(4).text()).toEqual('site.navbar.activity') expect(wrapper.findAll('ul').at(3).text()).toBe('logout')
// }) })
//
// it.skip('has fourth item "Activity" linked to activity in navbar', async () => { describe('logout', () => {
// navbar.findAll('ul > li > a').at(4).trigger('click') beforeEach(async () => {
// await flushPromises() await wrapper.findComponent({ name: 'sidebar' }).vm.$emit('logout')
// await jest.runAllTimers() await flushPromises()
// await flushPromises() })
// expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/activity')
// }) it('calls the API', async () => {
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: { sessionId: 1 },
}),
)
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toBeCalledWith('logout')
})
it('redirects to login page', () => {
expect(routerPushMock).toBeCalledWith('/login')
})
describe('logout fails', () => {
beforeEach(() => {
apolloMock.mockRejectedValue({
message: 'error',
})
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toBeCalledWith('logout')
})
it('redirects to login page', () => {
expect(routerPushMock).toBeCalledWith('/login')
})
})
})
describe('update balance', () => {
it('updates the amount correctelly', async () => {
await wrapper.findComponent({ ref: 'router-view' }).vm.$emit('update-balance', 5)
await flushPromises()
expect(wrapper.vm.balance).toBe(-5)
})
})
describe('update transactions', () => {
beforeEach(async () => {
apolloMock.mockResolvedValue({
data: {
transactionList: {
gdtSum: 100,
count: 4,
balance: 1450,
decay: 1250,
transactions: ['transaction', 'transaction', 'transaction', 'transaction'],
},
},
})
await wrapper
.findComponent({ ref: 'router-view' })
.vm.$emit('update-transactions', { firstPage: 2, items: 5 })
await flushPromises()
})
it('calls the API', () => {
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
firstPage: 2,
items: 5,
},
}),
)
})
it('updates balance', () => {
expect(wrapper.vm.balance).toBe(1250)
})
it('updates transactions', () => {
expect(wrapper.vm.transactions).toEqual([
'transaction',
'transaction',
'transaction',
'transaction',
])
})
it('updates GDT balance', () => {
expect(wrapper.vm.GdtBalance).toBe(100)
})
it('updates transaction count', () => {
expect(wrapper.vm.transactionCount).toBe(4)
})
})
describe('update transactions returns error', () => {
beforeEach(async () => {
apolloMock.mockRejectedValue({
message: 'Ouch!',
})
await wrapper
.findComponent({ ref: 'router-view' })
.vm.$emit('update-transactions', { firstPage: 2, items: 5 })
await flushPromises()
})
it('sets pending to true', () => {
expect(wrapper.vm.pending).toBeTruthy()
})
it('calls $toasted.error method', () => {
expect(toasterMock).toBeCalledWith('Ouch!')
})
})
}) })
}) })
}) })

View File

@ -50,6 +50,7 @@
<div @click="$sidebar.displaySidebar(false)"> <div @click="$sidebar.displaySidebar(false)">
<fade-transition :duration="200" origin="center top" mode="out-in"> <fade-transition :duration="200" origin="center top" mode="out-in">
<router-view <router-view
ref="router-view"
:balance="balance" :balance="balance"
:gdt-balance="GdtBalance" :gdt-balance="GdtBalance"
:transactions="transactions" :transactions="transactions"
@ -65,31 +66,11 @@
</div> </div>
</template> </template>
<script> <script>
import PerfectScrollbar from 'perfect-scrollbar' import { logout, transactionsQuery } from '../../graphql/queries'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
import loginAPI from '../../apis/loginAPI'
import ContentFooter from './ContentFooter.vue' import ContentFooter from './ContentFooter.vue'
import { FadeTransition } from 'vue2-transitions' import { FadeTransition } from 'vue2-transitions'
import communityAPI from '../../apis/communityAPI'
import VueQrcode from 'vue-qrcode' import VueQrcode from 'vue-qrcode'
function hasElement(className) {
return document.getElementsByClassName(className).length > 0
}
function initScrollbar(className) {
if (hasElement(className)) {
// eslint-disable-next-line no-new
new PerfectScrollbar(`.${className}`)
} else {
// try to init it later in case this component is loaded async
setTimeout(() => {
initScrollbar(className)
}, 100)
}
}
export default { export default {
components: { components: {
ContentFooter, ContentFooter,
@ -107,44 +88,55 @@ export default {
} }
}, },
methods: { methods: {
initScrollbar() {
const isWindows = navigator.platform.startsWith('Win')
if (isWindows) {
initScrollbar('sidenav')
}
},
async logout() { async logout() {
await loginAPI.logout(this.$store.state.sessionId) this.$apollo
// do we have to check success? .query({
this.$sidebar.displaySidebar(false) query: logout,
this.$store.dispatch('logout') variables: { sessionId: this.$store.state.sessionId },
this.$router.push('/login') })
.then(() => {
this.$sidebar.displaySidebar(false)
this.$store.dispatch('logout')
this.$router.push('/login')
})
.catch(() => {
this.$sidebar.displaySidebar(false)
this.$store.dispatch('logout')
this.$router.push('/login')
})
}, },
async updateTransactions(pagination) { async updateTransactions(pagination) {
this.pending = true this.pending = true
const result = await communityAPI.transactions( this.$apollo
this.$store.state.sessionId, .query({
pagination.firstPage, query: transactionsQuery,
pagination.items, variables: {
) sessionId: this.$store.state.sessionId,
if (result.success) { firstPage: pagination.firstPage,
this.GdtBalance = Number(result.result.data.gdtSum) items: pagination.items,
this.transactions = result.result.data.transactions },
this.balance = Number(result.result.data.decay) fetchPolicy: 'network-only',
this.bookedBalance = Number(result.result.data.balance) })
this.transactionCount = result.result.data.count .then((result) => {
this.pending = false const {
} else { data: { transactionList },
this.pending = true } = result
this.$toasted.error(result.result.message) this.GdtBalance = Number(transactionList.gdtSum)
} this.transactions = transactionList.transactions
this.balance = Number(transactionList.decay)
this.bookedBalance = Number(transactionList.balance)
this.transactionCount = transactionList.count
this.pending = false
})
.catch((error) => {
this.pending = true
this.$toasted.error(error.message)
// what to do when loading balance fails?
})
}, },
updateBalance(ammount) { updateBalance(ammount) {
this.balance -= ammount this.balance -= ammount
}, },
}, },
mounted() {
this.initScrollbar()
},
} }
</script> </script>

View File

@ -1,13 +1,8 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import AccountOverview from './AccountOverview' import AccountOverview from './AccountOverview'
import communityAPI from '../../apis/communityAPI.js'
jest.mock('../../apis/communityAPI.js')
const sendMock = jest.fn() const sendMock = jest.fn()
sendMock.mockReturnValue({ success: true }) sendMock.mockResolvedValue('success')
communityAPI.send = sendMock
const localVue = global.localVue const localVue = global.localVue
@ -27,6 +22,9 @@ describe('AccountOverview', () => {
}, },
}, },
$n: jest.fn((n) => String(n)), $n: jest.fn((n) => String(n)),
$apollo: {
query: sendMock,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -92,11 +90,16 @@ describe('AccountOverview', () => {
}) })
it('calls the API when send-transaction is emitted', async () => { it('calls the API when send-transaction is emitted', async () => {
expect(sendMock).toBeCalledWith(1, { expect(sendMock).toBeCalledWith(
email: 'user@example.org', expect.objectContaining({
amount: 23.45, variables: {
memo: 'Make the best of it!', sessionId: 1,
}) email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
},
}),
)
}) })
it('emits update-balance', () => { it('emits update-balance', () => {
@ -112,7 +115,7 @@ describe('AccountOverview', () => {
describe('transaction is confirmed and server response is error', () => { describe('transaction is confirmed and server response is error', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
sendMock.mockReturnValue({ success: false, result: { message: 'receiver not found' } }) sendMock.mockRejectedValue({ message: 'receiver not found' })
await wrapper await wrapper
.findComponent({ name: 'TransactionConfirmation' }) .findComponent({ name: 'TransactionConfirmation' })
.vm.$emit('send-transaction') .vm.$emit('send-transaction')

View File

@ -51,7 +51,7 @@ import GddTransactionListFooter from './AccountOverview/GddTransactionListFooter
import TransactionForm from './AccountOverview/GddSend/TransactionForm.vue' import TransactionForm from './AccountOverview/GddSend/TransactionForm.vue'
import TransactionConfirmation from './AccountOverview/GddSend/TransactionConfirmation.vue' import TransactionConfirmation from './AccountOverview/GddSend/TransactionConfirmation.vue'
import TransactionResult from './AccountOverview/GddSend/TransactionResult.vue' import TransactionResult from './AccountOverview/GddSend/TransactionResult.vue'
import communityAPI from '../../apis/communityAPI.js' import { sendCoins } from '../../graphql/queries.js'
const EMPTY_TRANSACTION_DATA = { const EMPTY_TRANSACTION_DATA = {
email: '', email: '',
@ -104,14 +104,22 @@ export default {
}, },
async sendTransaction() { async sendTransaction() {
this.loading = true this.loading = true
const result = await communityAPI.send(this.$store.state.sessionId, this.transactionData) this.$apollo
if (result.success) { .query({
this.error = false query: sendCoins,
this.$emit('update-balance', this.transactionData.amount) variables: {
} else { sessionId: this.$store.state.sessionId,
this.errorResult = result.result.message ...this.transactionData,
this.error = true },
} })
.then(() => {
this.error = false
this.$emit('update-balance', this.transactionData.amount)
})
.catch((err) => {
this.errorResult = err.message
this.error = true
})
this.currentTransactionStep = 2 this.currentTransactionStep = 2
this.loading = false this.loading = false
}, },

View File

@ -2,8 +2,8 @@
<div class="gdd-transaction-list"> <div class="gdd-transaction-list">
<div class="list-group"> <div class="list-group">
<div <div
v-for="{ decay, transaction_id, type, date, balance, name, memo } in transactions" v-for="{ decay, transactionId, type, date, balance, name, memo } in transactions"
:key="transaction_id" :key="transactionId"
:style="type === 'decay' ? 'background-color:#f1e0ae3d' : ''" :style="type === 'decay' ? 'background-color:#f1e0ae3d' : ''"
> >
<div class="list-group-item gdd-transaction-list-item" v-b-toggle="'a' + date + ''"> <div class="list-group-item gdd-transaction-list-item" v-b-toggle="'a' + date + ''">

View File

@ -6,16 +6,24 @@
</div> </div>
<div <div
v-else v-else
v-for="{ id, amount, date, comment, gdt_entry_type_id, factor, gdt } in transactionsGdt" v-for="{
:key="id" transactionId,
amount,
date,
comment,
gdtEntryType,
factor,
gdt,
} in transactionsGdt"
:key="transactionId"
> >
<div class="list-group-item gdt-transaction-list-item" v-b-toggle="'a' + date + ''"> <div class="list-group-item gdt-transaction-list-item" v-b-toggle="'a' + date + ''">
<!-- Icon --> <!-- Icon -->
<div class="text-right" style="position: absolute"> <div class="text-right" style="position: absolute">
<b-icon <b-icon
v-if="gdt_entry_type_id" v-if="gdtEntryType"
:icon="getIcon(gdt_entry_type_id).icon" :icon="getIcon(gdtEntryType).icon"
:class="getIcon(gdt_entry_type_id).class" :class="getIcon(gdtEntryType).class"
></b-icon> ></b-icon>
</div> </div>
@ -29,7 +37,7 @@
<!-- Betrag --> <!-- Betrag -->
<!-- 7 nur GDT erhalten --> <!-- 7 nur GDT erhalten -->
<b-row v-if="gdt_entry_type_id === 7"> <b-row v-if="gdtEntryType === 7">
<div class="col-6 text-right"> <div class="col-6 text-right">
<div>{{ $t('gdt.gdt-receive') }}</div> <div>{{ $t('gdt.gdt-receive') }}</div>
<div>{{ $t('gdt.credit') }}</div> <div>{{ $t('gdt.credit') }}</div>
@ -40,7 +48,7 @@
</div> </div>
</b-row> </b-row>
<!--4 publisher --> <!--4 publisher -->
<b-row v-else-if="gdt_entry_type_id === 4"> <b-row v-else-if="gdtEntryType === 4">
<div class="col-6 text-right"> <div class="col-6 text-right">
<div>{{ $t('gdt.your-share') }}</div> <div>{{ $t('gdt.your-share') }}</div>
<div>{{ $t('gdt.credit') }}</div> <div>{{ $t('gdt.credit') }}</div>
@ -65,7 +73,7 @@
<!-- Betrag ENDE--> <!-- Betrag ENDE-->
<!-- Nachricht--> <!-- Nachricht-->
<b-row v-if="comment && gdt_entry_type_id !== 7"> <b-row v-if="comment && gdtEntryType !== 7">
<div class="col-6 text-right"> <div class="col-6 text-right">
{{ $t('form.memo') }} {{ $t('form.memo') }}
</div> </div>
@ -87,21 +95,21 @@
<!-- Collaps START --> <!-- Collaps START -->
<b-collapse v-if="gdt_entry_type_id" :id="'a' + date + ''" class="pb-4"> <b-collapse v-if="gdtEntryType" :id="'a' + date + ''" class="pb-4">
<div style="border: 0px; background-color: #f1f1f1" class="p-2 pb-4 mb-4"> <div style="border: 0px; background-color: #f1f1f1" class="p-2 pb-4 mb-4">
<!-- Überschrift --> <!-- Überschrift -->
<b-row class="gdt-list-clooaps-header-text text-center pb-3"> <b-row class="gdt-list-clooaps-header-text text-center pb-3">
<div class="col h4" v-if="gdt_entry_type_id === 7"> <div class="col h4" v-if="gdtEntryType === 7">
{{ $t('gdt.conversion-gdt-euro') }} {{ $t('gdt.conversion-gdt-euro') }}
</div> </div>
<div class="col h4" v-else-if="gdt_entry_type_id === 4"> <div class="col h4" v-else-if="gdtEntryType === 4">
{{ $t('gdt.publisher') }} {{ $t('gdt.publisher') }}
</div> </div>
<div class="col h4" v-else>{{ $t('gdt.calculation') }}</div> <div class="col h4" v-else>{{ $t('gdt.calculation') }}</div>
</b-row> </b-row>
<!-- 7 nur GDT erhalten --> <!-- 7 nur GDT erhalten -->
<b-row class="gdt-list-clooaps-box-7" v-if="gdt_entry_type_id == 7"> <b-row class="gdt-list-clooaps-box-7" v-if="gdtEntryType == 7">
<div class="col-6 text-right clooaps-col-left"> <div class="col-6 text-right clooaps-col-left">
<div>{{ $t('gdt.raise') }}</div> <div>{{ $t('gdt.raise') }}</div>
<div>{{ $t('gdt.conversion') }}</div> <div>{{ $t('gdt.conversion') }}</div>
@ -115,7 +123,7 @@
</div> </div>
</b-row> </b-row>
<!-- 4 publisher --> <!-- 4 publisher -->
<b-row class="gdt-list-clooaps-box-4" v-else-if="gdt_entry_type_id === 4"> <b-row class="gdt-list-clooaps-box-4" v-else-if="gdtEntryType === 4">
<div class="col-6 text-right clooaps-col-left"></div> <div class="col-6 text-right clooaps-col-left"></div>
<div class="col-6 clooaps-col-right"></div> <div class="col-6 clooaps-col-right"></div>
</b-row> </b-row>
@ -152,7 +160,7 @@
</template> </template>
<script> <script>
import communityAPI from '../../../apis/communityAPI' import { listGDTEntriesQuery } from '../../../graphql/queries'
import PaginationButtons from '../../../components/PaginationButtons' import PaginationButtons from '../../../components/PaginationButtons'
const iconsByType = { const iconsByType = {
@ -187,17 +195,25 @@ export default {
}, },
methods: { methods: {
async updateGdt() { async updateGdt() {
const result = await communityAPI.transactionsgdt( this.$apollo
this.$store.state.sessionId, .query({
this.currentPage, query: listGDTEntriesQuery,
this.pageSize, variables: {
) sessionId: this.$store.state.sessionId,
if (result.success) { currentPage: this.currentPage,
this.transactionsGdt = result.result.data.gdtEntries pageSize: this.pageSize,
this.transactionGdtCount = result.result.data.count },
} else { })
this.$toasted.error(result.result.message) .then((result) => {
} const {
data: { listGDTEntries },
} = result
this.transactionsGdt = listGDTEntries.gdtEntries
this.transactionGdtCount = listGDTEntries.count
})
.catch((error) => {
this.$toasted.error(error.message)
})
}, },
getIcon(givenType) { getIcon(givenType) {
const type = iconsByType[givenType] const type = iconsByType[givenType]

View File

@ -1,12 +1,8 @@
import { mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import loginAPI from '../../apis/loginAPI.js'
import ForgotPassword from './ForgotPassword' import ForgotPassword from './ForgotPassword'
jest.mock('../../apis/loginAPI.js')
const mockAPIcall = jest.fn() const mockAPIcall = jest.fn()
loginAPI.sendEmail = mockAPIcall
const localVue = global.localVue const localVue = global.localVue
@ -20,6 +16,9 @@ describe('ForgotPassword', () => {
$router: { $router: {
push: mockRouterPush, push: mockRouterPush,
}, },
$apollo: {
query: mockAPIcall,
},
} }
const stubs = { const stubs = {
@ -92,18 +91,56 @@ describe('ForgotPassword', () => {
}) })
describe('valid Email', () => { describe('valid Email', () => {
beforeEach(async () => { beforeEach(() => {
await form.find('input').setValue('user@example.org') form.find('input').setValue('user@example.org')
await form.trigger('submit')
await flushPromises()
}) })
it('calls the API', () => { describe('calls the API', () => {
expect(mockAPIcall).toHaveBeenCalledWith('user@example.org') describe('sends back error', () => {
}) beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'error',
})
await form.trigger('submit')
await flushPromises()
})
it('pushes "/thx/password" to the route', () => { it('pushes to "/thx/password"', () => {
expect(mockRouterPush).toHaveBeenCalledWith('/thx/password') expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
},
}),
)
expect(mockRouterPush).toHaveBeenCalledWith('/thx/password')
})
})
describe('success', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
sendResetPasswordEmail: {
state: 'success',
},
},
})
await form.trigger('submit')
await flushPromises()
})
it('pushes to "/thx/password"', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
},
}),
)
expect(mockRouterPush).toHaveBeenCalledWith('/thx/password')
})
})
}) })
}) })
}) })

View File

@ -38,7 +38,7 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../apis/loginAPI.js' import { sendResetPasswordEmail } from '../../graphql/queries'
import InputEmail from '../../components/Inputs/InputEmail' import InputEmail from '../../components/Inputs/InputEmail'
export default { export default {
@ -56,9 +56,19 @@ export default {
}, },
methods: { methods: {
async onSubmit() { async onSubmit() {
await loginAPI.sendEmail(this.form.email) this.$apollo
// always give success to avoid email spying .query({
this.$router.push('/thx/password') query: sendResetPasswordEmail,
variables: {
email: this.form.email,
},
})
.then(() => {
this.$router.push('/thx/password')
})
.catch(() => {
this.$router.push('/thx/password')
})
}, },
}, },
} }

View File

@ -1,18 +1,13 @@
import { mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import loginAPI from '../../apis/loginAPI'
import Login from './Login' import Login from './Login'
jest.mock('../../apis/loginAPI')
const localVue = global.localVue const localVue = global.localVue
const mockLoginCall = jest.fn() const loginQueryMock = jest.fn().mockResolvedValue({
mockLoginCall.mockReturnValue({ data: {
success: true, login: {
result: { sessionId: 1,
data: {
session_id: 1,
user: { user: {
name: 'Peter Lustig', name: 'Peter Lustig',
}, },
@ -20,8 +15,6 @@ mockLoginCall.mockReturnValue({
}, },
}) })
loginAPI.login = mockLoginCall
const toastErrorMock = jest.fn() const toastErrorMock = jest.fn()
const mockStoreDispach = jest.fn() const mockStoreDispach = jest.fn()
const mockRouterPush = jest.fn() const mockRouterPush = jest.fn()
@ -52,6 +45,9 @@ describe('Login', () => {
$toasted: { $toasted: {
error: toastErrorMock, error: toastErrorMock,
}, },
$apollo: {
query: loginQueryMock,
},
} }
const stubs = { const stubs = {
@ -147,7 +143,14 @@ describe('Login', () => {
}) })
it('calls the API with the given data', () => { it('calls the API with the given data', () => {
expect(mockLoginCall).toBeCalledWith('user@example.org', '1234') expect(loginQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
password: '1234',
},
}),
)
}) })
it('creates a spinner', () => { it('creates a spinner', () => {
@ -173,7 +176,9 @@ describe('Login', () => {
describe('login fails', () => { describe('login fails', () => {
beforeEach(() => { beforeEach(() => {
mockLoginCall.mockReturnValue({ success: false }) loginQueryMock.mockRejectedValue({
message: 'Ouch!',
})
}) })
it('hides the spinner', () => { it('hides the spinner', () => {

View File

@ -55,10 +55,10 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../apis/loginAPI'
import CONFIG from '../../config' import CONFIG from '../../config'
import InputPassword from '../../components/Inputs/InputPassword' import InputPassword from '../../components/Inputs/InputPassword'
import InputEmail from '../../components/Inputs/InputEmail' import InputEmail from '../../components/Inputs/InputEmail'
import { login } from '../../graphql/queries'
export default { export default {
name: 'login', name: 'login',
@ -81,18 +81,26 @@ export default {
const loader = this.$loading.show({ const loader = this.$loading.show({
container: this.$refs.submitButton, container: this.$refs.submitButton,
}) })
const result = await loginAPI.login(this.form.email, this.form.password) this.$apollo
if (result.success) { .query({
this.$store.dispatch('login', { query: login,
sessionId: result.result.data.session_id, variables: {
user: result.result.data.user, email: this.form.email,
password: this.form.password,
},
})
.then((result) => {
const {
data: { login },
} = result
this.$store.dispatch('login', login)
this.$router.push('/overview')
loader.hide()
})
.catch(() => {
loader.hide()
this.$toasted.error(this.$t('error.no-account'))
}) })
this.$router.push('/overview')
loader.hide()
} else {
loader.hide()
this.$toasted.error(this.$t('error.no-account'))
}
}, },
}, },
} }

View File

@ -5,6 +5,9 @@ import Register from './Register'
const localVue = global.localVue const localVue = global.localVue
const resgisterUserQueryMock = jest.fn()
const routerPushMock = jest.fn()
describe('Register', () => { describe('Register', () => {
let wrapper let wrapper
@ -13,6 +16,12 @@ describe('Register', () => {
locale: 'en', locale: 'en',
}, },
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$router: {
push: routerPushMock,
},
$apollo: {
query: resgisterUserQueryMock,
},
} }
const stubs = { const stubs = {
@ -105,6 +114,113 @@ describe('Register', () => {
}) })
}) })
// To Do: Test lines 156-197,210-213 describe('resetForm', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
wrapper.find('input[name="site.signup.agree"]').setChecked(true)
})
it('resets the firstName field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('#registerFirstname').element.value).toBe('')
})
it('resets the lastName field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('#registerLastname').element.value).toBe('')
})
it('resets the email field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('#Email-input-field').element.value).toBe('')
})
it.skip('resets the password field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('input[name="form.password"]').element.value).toBe('')
})
it.skip('resets the passwordRepeat field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('input[name="form.passwordRepeat"]').element.value).toBe('')
})
it('resets the firstName field after clicking the reset button', async () => {
await wrapper.find('button.ml-2').trigger('click')
await flushPromises()
expect(wrapper.find('input[name="site.signup.agree"]').props.checked).not.toBeTruthy()
})
})
describe('API calls', () => {
beforeEach(() => {
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456')
})
describe('server sends back error', () => {
beforeEach(async () => {
resgisterUserQueryMock.mockRejectedValue({ message: 'Ouch!' })
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('shows error message', () => {
expect(wrapper.find('span.alert-text').exists()).toBeTruthy()
expect(wrapper.find('span.alert-text').text().length !== 0).toBeTruthy()
expect(wrapper.find('span.alert-text').text()).toContain('error.error!')
expect(wrapper.find('span.alert-text').text()).toContain('Ouch!')
})
it('button to dismisses error message is present', () => {
expect(wrapper.find('button.close').exists()).toBeTruthy()
})
it('dismisses error message', async () => {
await wrapper.find('button.close').trigger('click')
await flushPromises()
expect(wrapper.find('span.alert-text').exists()).not.toBeTruthy()
})
})
describe('server sends back success', () => {
beforeEach(() => {
resgisterUserQueryMock.mockResolvedValue({
data: {
create: 'success',
},
})
})
it('routes to "/thx/register"', async () => {
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(resgisterUserQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'max.mustermann@gradido.net',
firstName: 'Max',
lastName: 'Mustermann',
password: 'Aa123456',
},
}),
)
expect(routerPushMock).toHaveBeenCalledWith('/thx/register')
})
})
})
// TODO: line 157
}) })
}) })

View File

@ -128,9 +128,9 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../apis/loginAPI'
import InputEmail from '../../components/Inputs/InputEmail.vue' import InputEmail from '../../components/Inputs/InputEmail.vue'
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation.vue' import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation.vue'
import { resgisterUserQuery } from '../../graphql/queries'
export default { export default {
components: { InputPasswordConfirmation, InputEmail }, components: { InputPasswordConfirmation, InputEmail },
@ -168,38 +168,35 @@ export default {
}, },
agree: false, agree: false,
} }
this.form.password.password = ''
this.form.password.passwordRepeat = ''
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.observer.reset() this.$refs.observer.reset()
}) })
}, },
async onSubmit() { async onSubmit() {
const result = await loginAPI.create( this.$apollo
this.form.email, .query({
this.form.firstname, query: resgisterUserQuery,
this.form.lastname, variables: {
this.form.password.password,
)
if (result.success) {
/* this.$store.dispatch('login', {
sessionId: result.result.data.session_id,
user: {
email: this.form.email, email: this.form.email,
firstName: this.form.firstname, firstName: this.form.firstname,
lastName: this.form.lastname lastName: this.form.lastname,
} password: this.form.password.password,
},
})
.then(() => {
this.form.email = ''
this.form.firstname = ''
this.form.lastname = ''
this.form.password.password = ''
this.form.password.passwordRepeat = ''
this.$router.push('/thx/register')
})
.catch((error) => {
this.showError = true
this.messageError = error.message
}) })
*/
this.form.email = ''
this.form.firstname = ''
this.form.lastname = ''
this.form.password.password = ''
this.$router.push('/thx/register')
} else {
this.showError = true
this.messageError = result.result.message
}
}, },
closeAlert() { closeAlert() {
this.showError = false this.showError = false
@ -208,6 +205,7 @@ export default {
this.form.firstname = '' this.form.firstname = ''
this.form.lastname = '' this.form.lastname = ''
this.form.password.password = '' this.form.password.password = ''
this.form.password.passwordRepeat = ''
}, },
}, },
computed: { computed: {

View File

@ -1,45 +1,16 @@
import { mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import loginAPI from '../../apis/loginAPI'
import ResetPassword from './ResetPassword' import ResetPassword from './ResetPassword'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js // validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js
jest.mock('../../apis/loginAPI')
const localVue = global.localVue const localVue = global.localVue
const successResponseObject = { const apolloQueryMock = jest.fn().mockRejectedValue({ message: 'error' })
success: true,
result: {
data: {
session_id: 1,
user: {
email: 'user@example.org',
},
},
},
}
const emailVerificationMock = jest.fn()
const changePasswordMock = jest.fn()
const toasterMock = jest.fn() const toasterMock = jest.fn()
const routerPushMock = jest.fn() const routerPushMock = jest.fn()
emailVerificationMock
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValue(successResponseObject)
changePasswordMock
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValue({ success: true })
loginAPI.loginViaEmailVerificationCode = emailVerificationMock
loginAPI.changePassword = changePasswordMock
describe('ResetPassword', () => { describe('ResetPassword', () => {
let wrapper let wrapper
@ -64,6 +35,9 @@ describe('ResetPassword', () => {
return { hide: jest.fn() } return { hide: jest.fn() }
}), }),
}, },
$apollo: {
query: apolloQueryMock,
},
} }
const stubs = { const stubs = {
@ -79,88 +53,129 @@ describe('ResetPassword', () => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('calls the email verification when created', () => { it('calls the email verification when created', async () => {
expect(emailVerificationMock).toHaveBeenCalledWith('123') expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({ variables: { optin: '123' } }),
)
}) })
it('does not render the Reset Password form when not authenticated', () => { describe('No valid optin', () => {
expect(wrapper.find('form').exists()).toBeFalsy() it('does not render the Reset Password form when not authenticated', () => {
}) expect(wrapper.find('form').exists()).toBeFalsy()
})
it('toasts an error when no valid optin is given', () => { it('toasts an error when no valid optin is given', () => {
expect(toasterMock).toHaveBeenCalledWith('error') expect(toasterMock).toHaveBeenCalledWith('error')
}) })
it('has a message suggesting to contact the support', () => { it('has a message suggesting to contact the support', () => {
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
expect(wrapper.find('div.header').text()).toContain('reset-password.not-authenticated')
})
it('renders the Reset Password form when authenticated', async () => {
await wrapper.setData({ authenticated: true })
expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy()
})
describe('Register header', () => {
it('has a welcome message', () => {
expect(wrapper.find('div.header').text()).toContain('reset-password.title') expect(wrapper.find('div.header').text()).toContain('reset-password.title')
expect(wrapper.find('div.header').text()).toContain('reset-password.text') expect(wrapper.find('div.header').text()).toContain('reset-password.not-authenticated')
}) })
}) })
describe('links', () => { describe('is authenticated', () => {
it('has a link "Back"', () => { beforeEach(() => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back') apolloQueryMock.mockResolvedValue({
}) data: {
loginViaEmailVerificationCode: {
it('links to /login when clicking "Back"', () => { sessionId: 1,
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/Login') email: 'user@example.org',
}) },
}) },
describe('reset password form', () => {
it('has a register form', () => {
expect(wrapper.find('form').exists()).toBeTruthy()
})
it('has 2 password input fields', () => {
expect(wrapper.findAll('input[type="password"]').length).toBe(2)
})
it('toggles the first input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(0).trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.findAll('input').at(0).attributes('type')).toBe('text')
})
it('toggles the second input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(1).trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.findAll('input').at(1).attributes('type')).toBe('text')
})
})
describe('submit form', () => {
beforeEach(async () => {
await wrapper.findAll('input').at(0).setValue('Aa123456')
await wrapper.findAll('input').at(1).setValue('Aa123456')
await flushPromises()
await wrapper.find('form').trigger('submit')
})
describe('server response with error', () => {
it('toasts an error message', () => {
expect(toasterMock).toHaveBeenCalledWith('error')
}) })
}) })
describe('server response with success', () => { it.skip('Has sessionId from API call', async () => {
it('calls the API', () => { await wrapper.vm.$nextTick()
expect(changePasswordMock).toHaveBeenCalledWith(1, 'user@example.org', 'Aa123456') expect(wrapper.vm.sessionId).toBe(1)
})
it('renders the Reset Password form when authenticated', () => {
expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy()
})
describe('Register header', () => {
it('has a welcome message', async () => {
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
expect(wrapper.find('div.header').text()).toContain('reset-password.text')
})
})
describe('links', () => {
it('has a link "Back"', async () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back')
}) })
it('redirects to "/thx/reset"', () => { it('links to /login when clicking "Back"', async () => {
expect(routerPushMock).toHaveBeenCalledWith('/thx/reset') expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/Login')
})
})
describe('reset password form', () => {
it('has a register form', async () => {
expect(wrapper.find('form').exists()).toBeTruthy()
})
it('has 2 password input fields', async () => {
expect(wrapper.findAll('input[type="password"]').length).toBe(2)
})
it('toggles the first input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(0).trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.findAll('input').at(0).attributes('type')).toBe('text')
})
it('toggles the second input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(1).trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.findAll('input').at(1).attributes('type')).toBe('text')
})
})
describe('submit form', () => {
beforeEach(async () => {
await wrapper.setData({ authenticated: true, sessionId: 1 })
await wrapper.vm.$nextTick()
await wrapper.findAll('input').at(0).setValue('Aa123456')
await wrapper.findAll('input').at(1).setValue('Aa123456')
await flushPromises()
await wrapper.find('form').trigger('submit')
})
describe('server response with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'error' })
})
it('toasts an error message', () => {
expect(toasterMock).toHaveBeenCalledWith('error')
})
})
describe('server response with success', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
resetPassword: 'success',
},
})
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
password: 'Aa123456',
},
}),
)
})
it('redirects to "/thx/reset"', () => {
expect(routerPushMock).toHaveBeenCalledWith('/thx/reset')
})
}) })
}) })
}) })

View File

@ -47,8 +47,8 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../apis/loginAPI'
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation' import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation'
import { resetPassword, loginViaEmailVerificationCode } from '../../graphql/queries'
export default { export default {
name: 'ResetPassword', name: 'ResetPassword',
@ -70,33 +70,43 @@ export default {
}, },
methods: { methods: {
async onSubmit() { async onSubmit() {
const result = await loginAPI.changePassword(this.sessionId, this.email, this.form.password) this.$apollo
if (result.success) { .query({
this.form.password = '' query: resetPassword,
/* variables: {
this.$store.dispatch('login', { sessionId: this.sessionId,
sessionId: result.result.data.session_id, email: this.email,
email: result.result.data.user.email, password: this.form.password,
}) },
*/ })
this.$router.push('/thx/reset') .then(() => {
} else { this.form.password = ''
this.$toasted.error(result.result.message) this.$router.push('/thx/reset')
} })
.catch((error) => {
this.$toasted.error(error.message)
})
}, },
async authenticate() { async authenticate() {
const loader = this.$loading.show({ const loader = this.$loading.show({
container: this.$refs.header, container: this.$refs.header,
}) })
const optin = this.$route.params.optin const optin = this.$route.params.optin
const result = await loginAPI.loginViaEmailVerificationCode(optin) this.$apollo
if (result.success) { .query({
this.authenticated = true query: loginViaEmailVerificationCode,
this.sessionId = result.result.data.session_id variables: {
this.email = result.result.data.user.email optin: optin,
} else { },
this.$toasted.error(result.result.message) })
} .then((result) => {
this.authenticated = true
this.sessionId = result.data.loginViaEmailVerificationCode.sessionId
this.email = result.data.loginViaEmailVerificationCode.email
})
.catch((error) => {
this.$toasted.error(error.message)
})
loader.hide() loader.hide()
this.pending = false this.pending = false
}, },

View File

@ -1,23 +1,16 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import UserCardFormUserData from './UserCard_FormUserData' import UserCardFormUserData from './UserCard_FormUserData'
import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue const localVue = global.localVue
const mockAPIcall = jest.fn((args) => { const mockAPIcall = jest.fn()
return { success: true }
})
const toastErrorMock = jest.fn() const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn() const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
loginAPI.updateUserInfos = mockAPIcall describe('UserCard_FormUserData', () => {
describe('UserCard_FormUsername', () => {
let wrapper let wrapper
const mocks = { const mocks = {
@ -36,6 +29,9 @@ describe('UserCard_FormUsername', () => {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, error: toastErrorMock,
}, },
$apollo: {
query: mockAPIcall,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -102,6 +98,13 @@ describe('UserCard_FormUsername', () => {
describe('successfull submit', () => { describe('successfull submit', () => {
beforeEach(async () => { beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
})
jest.clearAllMocks() jest.clearAllMocks()
await wrapper.findAll('input').at(0).setValue('Petra') await wrapper.findAll('input').at(0).setValue('Petra')
await wrapper.findAll('input').at(1).setValue('Lustiger') await wrapper.findAll('input').at(1).setValue('Lustiger')
@ -111,12 +114,18 @@ describe('UserCard_FormUsername', () => {
await flushPromises() await flushPromises()
}) })
it('calls the loginAPI', () => { it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(1, 'user@example.org', { expect(mockAPIcall).toBeCalledWith(
firstName: 'Petra', expect.objectContaining({
lastName: 'Lustiger', variables: {
description: 'Keine Nickelbrille', sessionId: 1,
}) email: 'user@example.org',
firstName: 'Petra',
lastName: 'Lustiger',
description: 'Keine Nickelbrille',
},
}),
)
}) })
it('commits firstname to store', () => { it('commits firstname to store', () => {
@ -142,8 +151,10 @@ describe('UserCard_FormUsername', () => {
describe('submit results in server error', () => { describe('submit results in server error', () => {
beforeEach(async () => { beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Error',
})
jest.clearAllMocks() jest.clearAllMocks()
mockAPIcall.mockReturnValue({ success: false, result: { message: 'Error' } })
await wrapper.findAll('input').at(0).setValue('Petra') await wrapper.findAll('input').at(0).setValue('Petra')
await wrapper.findAll('input').at(1).setValue('Lustiger') await wrapper.findAll('input').at(1).setValue('Lustiger')
await wrapper.find('textarea').setValue('Keine Nickelbrille') await wrapper.find('textarea').setValue('Keine Nickelbrille')
@ -152,12 +163,18 @@ describe('UserCard_FormUsername', () => {
await flushPromises() await flushPromises()
}) })
it('calls the loginAPI', () => { it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(1, 'user@example.org', { expect(mockAPIcall).toBeCalledWith(
firstName: 'Petra', expect.objectContaining({
lastName: 'Lustiger', variables: {
description: 'Keine Nickelbrille', sessionId: 1,
}) email: 'user@example.org',
firstName: 'Petra',
lastName: 'Lustiger',
description: 'Keine Nickelbrille',
},
}),
)
}) })
it('toasts an error message', () => { it('toasts an error message', () => {

View File

@ -78,7 +78,7 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../../apis/loginAPI' import { updateUserInfos } from '../../../graphql/queries'
export default { export default {
name: 'FormUserData', name: 'FormUserData',
@ -114,24 +114,27 @@ export default {
}, },
async onSubmit(event) { async onSubmit(event) {
event.preventDefault() event.preventDefault()
const result = await loginAPI.updateUserInfos( this.$apollo
this.$store.state.sessionId, .query({
this.$store.state.email, query: updateUserInfos,
{ variables: {
firstName: this.form.firstName, sessionId: this.$store.state.sessionId,
lastName: this.form.lastName, email: this.$store.state.email,
description: this.form.description, firstName: this.form.firstName,
}, lastName: this.form.lastName,
) description: this.form.description,
if (result.success) { },
this.$store.commit('firstName', this.form.firstName) })
this.$store.commit('lastName', this.form.lastName) .then(() => {
this.$store.commit('description', this.form.description) this.$store.commit('firstName', this.form.firstName)
this.showUserData = true this.$store.commit('lastName', this.form.lastName)
this.$toasted.success(this.$t('site.profil.user-data.change-success')) this.$store.commit('description', this.form.description)
} else { this.showUserData = true
this.$toasted.error(result.result.message) this.$toasted.success(this.$t('site.profil.user-data.change-success'))
} })
.catch((error) => {
this.$toasted.error(error.message)
})
}, },
}, },
} }

View File

@ -0,0 +1,123 @@
import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import UserCardFormUserMail from './UserCard_FormUserMail'
const localVue = global.localVue
jest.spyOn(window, 'alert').mockImplementation(() => {})
const mockAPIcall = jest.fn()
describe('UserCard_FormUserMail', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Peter',
lastName: 'Lustig',
description: '',
},
},
$apollo: {
query: mockAPIcall,
},
}
const Wrapper = () => {
return mount(UserCardFormUserMail, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#formusermail').exists()).toBeTruthy()
})
it('renders the edit link', () => {
expect(wrapper.find('a[href="#formusermail"]').exists()).toBeTruthy()
})
it('renders the E-Mail form.change', () => {
expect(wrapper.findAll('div.col').at(0).text()).toBe('E-Mail form.change')
})
it('renders the E-Mail', () => {
expect(wrapper.findAll('div.col').at(1).text()).toBe('E-Mail')
})
it('renders the E-Mail Adress', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('user@example.org')
})
describe('edit user data', () => {
beforeEach(async () => {
await wrapper.find('a[href="#formusermail"]').trigger('click')
await flushPromises()
await wrapper.findAll('input').at(0).setValue('test@example.org')
await flushPromises()
})
describe('error API send', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Ouch!',
})
await wrapper.find('a[href="#formusermail"]').trigger('click')
await flushPromises()
})
it('sends request with filled variables to the API', async () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
newEmail: 'test@example.org',
},
}),
)
})
it('shows an error message', async () => {
expect(window.alert).toBeCalledWith('Ouch!')
})
})
describe('successful API send', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('a[href="#formusermail"]').trigger('click')
await flushPromises()
})
it('sends request with filled variables to the API', async () => {
expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
sessionId: 1,
email: 'user@example.org',
newEmail: 'test@example.org',
},
}),
)
})
it('sends a success message', async () => {
expect(window.alert).toBeCalledWith('changePassword success')
})
})
})
})
})

View File

@ -32,7 +32,7 @@
</b-card> </b-card>
</template> </template>
<script> <script>
import loginAPI from '../../../apis/loginAPI' import { updateUserInfos } from '../../../graphql/queries'
export default { export default {
name: 'FormUserMail', name: 'FormUserMail',
@ -44,17 +44,21 @@ export default {
}, },
methods: { methods: {
async onSubmit() { async onSubmit() {
// console.log(this.data) this.$apollo
const result = await loginAPI.changeEmailProfil( .query({
this.$store.state.sessionId, query: updateUserInfos,
this.email, variables: {
this.newEmail, sessionId: this.$store.state.sessionId,
) email: this.$store.state.email,
if (result.success) { newEmail: this.newEmail,
alert('changePassword success') },
} else { })
alert(result.result.message) .then(() => {
} alert('changePassword success')
})
.catch((error) => {
alert(error.message)
})
}, },
}, },
} }

View File

@ -1,21 +1,16 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import UserCardFormPasswort from './UserCard_FormUserPasswort' import UserCardFormPasswort from './UserCard_FormUserPasswort'
import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue const localVue = global.localVue
const changePasswordProfileMock = jest.fn() const changePasswordProfileMock = jest.fn()
changePasswordProfileMock.mockReturnValue({ success: true }) changePasswordProfileMock.mockReturnValue({ success: true })
loginAPI.changePasswordProfile = changePasswordProfileMock
const toastSuccessMock = jest.fn() const toastSuccessMock = jest.fn()
const toastErrorMock = jest.fn() const toastErrorMock = jest.fn()
describe('UserCardFormUserPasswort', () => { describe('UserCard_FormUserPasswort', () => {
let wrapper let wrapper
const mocks = { const mocks = {
@ -30,6 +25,9 @@ describe('UserCardFormUserPasswort', () => {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, error: toastErrorMock,
}, },
$apollo: {
query: changePasswordProfileMock,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -159,6 +157,13 @@ describe('UserCardFormUserPasswort', () => {
describe('submit', () => { describe('submit', () => {
describe('valid data', () => { describe('valid data', () => {
beforeEach(async () => { beforeEach(async () => {
changePasswordProfileMock.mockResolvedValue({
data: {
updateUserData: {
validValues: 1,
},
},
})
await form.findAll('input').at(0).setValue('1234') await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456') await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456') await form.findAll('input').at(2).setValue('Aa123456')
@ -168,10 +173,14 @@ describe('UserCardFormUserPasswort', () => {
it('calls the API', () => { it('calls the API', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith( expect(changePasswordProfileMock).toHaveBeenCalledWith(
1, expect.objectContaining({
'user@example.org', variables: {
'1234', sessionId: 1,
'Aa123456', email: 'user@example.org',
password: '1234',
passwordNew: 'Aa123456',
},
}),
) )
}) })
@ -186,9 +195,8 @@ describe('UserCardFormUserPasswort', () => {
describe('server response is error', () => { describe('server response is error', () => {
beforeEach(async () => { beforeEach(async () => {
changePasswordProfileMock.mockReturnValue({ changePasswordProfileMock.mockRejectedValue({
success: false, message: 'error',
result: { message: 'error' },
}) })
await form.findAll('input').at(0).setValue('1234') await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456') await form.findAll('input').at(1).setValue('Aa123456')

View File

@ -45,9 +45,9 @@
</div> </div>
</template> </template>
<script> <script>
import loginAPI from '../../../apis/loginAPI'
import InputPassword from '../../../components/Inputs/InputPassword' import InputPassword from '../../../components/Inputs/InputPassword'
import InputPasswordConfirmation from '../../../components/Inputs/InputPasswordConfirmation' import InputPasswordConfirmation from '../../../components/Inputs/InputPasswordConfirmation'
import { updateUserInfos } from '../../../graphql/queries'
export default { export default {
name: 'FormUserPasswort', name: 'FormUserPasswort',
@ -77,18 +77,23 @@ export default {
this.form.passwordNewRepeat = '' this.form.passwordNewRepeat = ''
}, },
async onSubmit() { async onSubmit() {
const result = await loginAPI.changePasswordProfile( this.$apollo
this.$store.state.sessionId, .query({
this.$store.state.email, query: updateUserInfos,
this.form.password, variables: {
this.form.newPassword.password, sessionId: this.$store.state.sessionId,
) email: this.$store.state.email,
if (result.success) { password: this.form.password,
this.$toasted.success(this.$t('site.thx.reset')) passwordNew: this.form.newPassword.password,
this.cancelEdit() },
} else { })
this.$toasted.error(result.result.message) .then(() => {
} this.$toasted.success(this.$t('site.thx.reset'))
this.cancelEdit()
})
.catch((error) => {
this.$toasted.error(error.message)
})
}, },
}, },
} }

View File

@ -1,16 +1,11 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import UserCardFormUsername from './UserCard_FormUsername' import UserCardFormUsername from './UserCard_FormUsername'
import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { extend } from 'vee-validate' import { extend } from 'vee-validate'
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue const localVue = global.localVue
const mockAPIcall = jest.fn((args) => { const mockAPIcall = jest.fn()
return { success: true }
})
// override this rule to avoid API call // override this rule to avoid API call
extend('gddUsernameUnique', { extend('gddUsernameUnique', {
@ -23,8 +18,6 @@ const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn() const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
loginAPI.changeUsernameProfile = mockAPIcall
describe('UserCard_FormUsername', () => { describe('UserCard_FormUsername', () => {
let wrapper let wrapper
@ -42,6 +35,9 @@ describe('UserCard_FormUsername', () => {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, error: toastErrorMock,
}, },
$apollo: {
query: mockAPIcall,
},
} }
const Wrapper = () => { const Wrapper = () => {
@ -98,13 +94,28 @@ describe('UserCard_FormUsername', () => {
describe('successfull submit', () => { describe('successfull submit', () => {
beforeEach(async () => { beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('input[placeholder="Username"]').setValue('username') await wrapper.find('input[placeholder="Username"]').setValue('username')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
}) })
it('calls the loginAPI', () => { it('calls the API', () => {
expect(mockAPIcall).toHaveBeenCalledWith(1, 'user@example.org', 'username') expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
sessionId: 1,
username: 'username',
},
}),
)
}) })
it('displays the new username', () => { it('displays the new username', () => {
@ -127,21 +138,28 @@ describe('UserCard_FormUsername', () => {
describe('submit retruns error', () => { describe('submit retruns error', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
mockAPIcall.mockReturnValue({ mockAPIcall.mockRejectedValue({
success: false, message: 'Ouch!',
result: { message: 'Error' },
}) })
await wrapper.find('input[placeholder="Username"]').setValue('username') await wrapper.find('input[placeholder="Username"]').setValue('username')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
}) })
it('calls the loginAPI', () => { it('calls the API', () => {
expect(mockAPIcall).toHaveBeenCalledWith(1, 'user@example.org', 'username') expect(mockAPIcall).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
email: 'user@example.org',
sessionId: 1,
username: 'username',
},
}),
)
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Error') expect(toastErrorMock).toBeCalledWith('Ouch!')
}) })
it('renders an empty username', () => { it('renders an empty username', () => {

View File

@ -67,7 +67,7 @@
</b-card> </b-card>
</template> </template>
<script> <script>
import loginAPI from '../../../apis/loginAPI' import { updateUserInfos } from '../../../graphql/queries'
export default { export default {
name: 'FormUsername', name: 'FormUsername',
@ -86,22 +86,27 @@ export default {
this.showUsername = true this.showUsername = true
}, },
async onSubmit() { async onSubmit() {
const result = await loginAPI.changeUsernameProfile( this.$apollo
this.$store.state.sessionId, .query({
this.$store.state.email, query: updateUserInfos,
this.form.username, variables: {
) sessionId: this.$store.state.sessionId,
if (result.success) { email: this.$store.state.email,
this.$store.commit('username', this.form.username) username: this.form.username,
this.username = this.form.username },
this.showUsername = true })
this.$toasted.success(this.$t('site.profil.user-data.change-success')) .then(() => {
} else { this.$store.commit('username', this.form.username)
this.$toasted.error(result.result.message) this.username = this.form.username
this.showUsername = true this.showUsername = true
this.username = this.$store.state.username this.$toasted.success(this.$t('site.profil.user-data.change-success'))
this.form.username = this.$store.state.username })
} .catch((error) => {
this.$toasted.error(error.message)
this.showUsername = true
this.username = this.$store.state.username
this.form.username = this.$store.state.username
})
}, },
}, },
} }

View File

@ -1811,6 +1811,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
"@types/node@>=6":
version "16.4.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.12.tgz#961e3091f263e6345d2d84afab4e047a60b4b11b"
integrity sha512-zxrTNFl9Z8boMJXs6ieqZP0wAhvkdzmHSxTlJabM16cf5G9xBc1uPRH5Bbv2omEDDiM8MzTfqTJXBf0Ba4xFWA==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@ -1865,6 +1870,11 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@types/zen-observable@^0.8.0":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3"
integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==
"@typescript-eslint/experimental-utils@^4.0.1": "@typescript-eslint/experimental-utils@^4.0.1":
version "4.19.0" version "4.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.19.0.tgz#9ca379919906dc72cb0fcd817d6cb5aa2d2054c6" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.19.0.tgz#9ca379919906dc72cb0fcd817d6cb5aa2d2054c6"
@ -2349,6 +2359,21 @@
"@webassemblyjs/wast-parser" "1.9.0" "@webassemblyjs/wast-parser" "1.9.0"
"@xtuc/long" "4.2.2" "@xtuc/long" "4.2.2"
"@wry/context@^0.4.0":
version "0.4.4"
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8"
integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==
dependencies:
"@types/node" ">=6"
tslib "^1.9.3"
"@wry/equality@^0.1.2":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==
dependencies:
tslib "^1.9.3"
"@xtuc/ieee754@^1.2.0": "@xtuc/ieee754@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -2587,6 +2612,101 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
apollo-boost@^0.4.9:
version "0.4.9"
resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.9.tgz#ab3ba539c2ca944e6fd156583a1b1954b17a6791"
integrity sha512-05y5BKcDaa8w47f8d81UVwKqrAjn8uKLv6QM9fNdldoNzQ+rnOHgFlnrySUZRz9QIT3vPftQkEz2UEASp1Mi5g==
dependencies:
apollo-cache "^1.3.5"
apollo-cache-inmemory "^1.6.6"
apollo-client "^2.6.10"
apollo-link "^1.0.6"
apollo-link-error "^1.0.3"
apollo-link-http "^1.3.1"
graphql-tag "^2.4.2"
ts-invariant "^0.4.0"
tslib "^1.10.0"
apollo-cache-inmemory@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz#56d1f2a463a6b9db32e9fa990af16d2a008206fd"
integrity sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==
dependencies:
apollo-cache "^1.3.5"
apollo-utilities "^1.3.4"
optimism "^0.10.0"
ts-invariant "^0.4.0"
tslib "^1.10.0"
apollo-cache@1.3.5, apollo-cache@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.5.tgz#9dbebfc8dbe8fe7f97ba568a224bca2c5d81f461"
integrity sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==
dependencies:
apollo-utilities "^1.3.4"
tslib "^1.10.0"
apollo-client@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.10.tgz#86637047b51d940c8eaa771a4ce1b02df16bea6a"
integrity sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.5"
apollo-link "^1.0.0"
apollo-utilities "1.3.4"
symbol-observable "^1.0.2"
ts-invariant "^0.4.0"
tslib "^1.10.0"
zen-observable "^0.8.0"
apollo-link-error@^1.0.3:
version "1.1.13"
resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.13.tgz#c1a1bb876ffe380802c8df0506a32c33aad284cd"
integrity sha512-jAZOOahJU6bwSqb2ZyskEK1XdgUY9nkmeclCrW7Gddh1uasHVqmoYc4CKdb0/H0Y1J9lvaXKle2Wsw/Zx1AyUg==
dependencies:
apollo-link "^1.2.14"
apollo-link-http-common "^0.2.16"
tslib "^1.9.3"
apollo-link-http-common@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz#756749dafc732792c8ca0923f9a40564b7c59ecc"
integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==
dependencies:
apollo-link "^1.2.14"
ts-invariant "^0.4.0"
tslib "^1.9.3"
apollo-link-http@^1.3.1:
version "1.5.17"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.17.tgz#499e9f1711bf694497f02c51af12d82de5d8d8ba"
integrity sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==
dependencies:
apollo-link "^1.2.14"
apollo-link-http-common "^0.2.16"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.14:
version "1.2.14"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9"
integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==
dependencies:
apollo-utilities "^1.3.0"
ts-invariant "^0.4.0"
tslib "^1.9.3"
zen-observable-ts "^0.8.21"
apollo-utilities@1.3.4, apollo-utilities@^1.3.0, apollo-utilities@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf"
integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==
dependencies:
"@wry/equality" "^0.1.2"
fast-json-stable-stringify "^2.0.0"
ts-invariant "^0.4.0"
tslib "^1.10.0"
aproba@^1.0.3, aproba@^1.1.1: aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -6796,6 +6916,18 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
graphql-tag@^2.4.2:
version "2.12.5"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.5.tgz#5cff974a67b417747d05c8d9f5f3cb4495d0db8f"
integrity sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ==
dependencies:
tslib "^2.1.0"
graphql@^15.5.1:
version "15.5.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.1.tgz#f2f84415d8985e7b84731e7f3536f8bb9d383aad"
integrity sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw==
growly@^1.3.0: growly@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@ -9979,6 +10111,13 @@ opn@^5.5.0:
dependencies: dependencies:
is-wsl "^1.1.0" is-wsl "^1.1.0"
optimism@^0.10.0:
version "0.10.3"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7"
integrity sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==
dependencies:
"@wry/context" "^0.4.0"
optimist@0.3: optimist@0.3:
version "0.3.7" version "0.3.7"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9"
@ -12548,6 +12687,11 @@ sweetalert2@^9.5.4:
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-9.17.2.tgz#7f33ce157a64c303d2ca98863c9a3f437e5f1187" resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-9.17.2.tgz#7f33ce157a64c303d2ca98863c9a3f437e5f1187"
integrity sha512-HkpPZVMYsnhFUBLdy/LvkU9snggKP3VAuSVnPhVXjxdg02lWbFx0W8H3m7A+WMWw2diXZS1wIa4m67XkNxdvew== integrity sha512-HkpPZVMYsnhFUBLdy/LvkU9snggKP3VAuSVnPhVXjxdg02lWbFx0W8H3m7A+WMWw2diXZS1wIa4m67XkNxdvew==
symbol-observable@^1.0.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
symbol-tree@^3.2.2, symbol-tree@^3.2.4: symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -12680,6 +12824,11 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throttle-debounce@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
through2@^2.0.0, through2@~2.0.3: through2@^2.0.0, through2@~2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@ -12853,6 +13002,13 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-invariant@^0.4.0:
version "0.4.4"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
dependencies:
tslib "^1.9.3"
ts-jest@^24.2.0: ts-jest@^24.2.0:
version "24.3.0" version "24.3.0"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869"
@ -12889,11 +13045,16 @@ tsconfig@^7.0.0:
strip-bom "^3.0.0" strip-bom "^3.0.0"
strip-json-comments "^2.0.0" strip-json-comments "^2.0.0"
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tsutils@^3.17.1: tsutils@^3.17.1:
version "3.21.0" version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@ -13268,6 +13429,15 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-apollo@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.7.tgz#97a031d45641faa4888a6d5a7f71c40834359704"
integrity sha512-EUfIn4cJmoflnDJiSNP8gH4fofIEzd0I2AWnd9nhHB8mddmzIfgSNjIRihDcRB10wypYG1OG0GcU335CFgZRfA==
dependencies:
chalk "^2.4.2"
serialize-javascript "^4.0.0"
throttle-debounce "^2.1.0"
vue-bootstrap-typeahead@^0.2.6: vue-bootstrap-typeahead@^0.2.6:
version "0.2.6" version "0.2.6"
resolved "https://registry.yarnpkg.com/vue-bootstrap-typeahead/-/vue-bootstrap-typeahead-0.2.6.tgz#8c1999a00bf4bf9fc906bae3a462482482cbc297" resolved "https://registry.yarnpkg.com/vue-bootstrap-typeahead/-/vue-bootstrap-typeahead-0.2.6.tgz#8c1999a00bf4bf9fc906bae3a462482482cbc297"
@ -14019,3 +14189,16 @@ yorkie@^2.0.0:
is-ci "^1.0.10" is-ci "^1.0.10"
normalize-path "^1.0.0" normalize-path "^1.0.0"
strip-indent "^2.0.0" strip-indent "^2.0.0"
zen-observable-ts@^0.8.21:
version "0.8.21"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d"
integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==
dependencies:
tslib "^1.9.3"
zen-observable "^0.8.0"
zen-observable@^0.8.0:
version "0.8.15"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==