mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into login_call_unsecureLogin
This commit is contained in:
commit
0341ae0d13
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -344,7 +344,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: 85
|
min_coverage: 86
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
@ -394,7 +394,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 39
|
min_coverage: 38
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import { ObjectType, Field } from 'type-graphql'
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class UpdateUserInfosResponse {
|
|
||||||
constructor(json: any) {
|
|
||||||
this.validValues = json.valid_values
|
|
||||||
}
|
|
||||||
|
|
||||||
@Field(() => Number)
|
|
||||||
validValues: number
|
|
||||||
}
|
|
||||||
@ -48,7 +48,7 @@ describe('CommunityResolver', () => {
|
|||||||
|
|
||||||
describe('getCommunityInfo', () => {
|
describe('getCommunityInfo', () => {
|
||||||
it('returns the default values', async () => {
|
it('returns the default values', async () => {
|
||||||
expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
|
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
getCommunityInfo: {
|
getCommunityInfo: {
|
||||||
name: 'Gradido Entwicklung',
|
name: 'Gradido Entwicklung',
|
||||||
@ -68,7 +68,7 @@ describe('CommunityResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns three communities', async () => {
|
it('returns three communities', async () => {
|
||||||
expect(query({ query: communities })).resolves.toMatchObject({
|
await expect(query({ query: communities })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
communities: [
|
communities: [
|
||||||
{
|
{
|
||||||
@ -104,7 +104,7 @@ describe('CommunityResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns one community', async () => {
|
it('returns one community', async () => {
|
||||||
expect(query({ query: communities })).resolves.toMatchObject({
|
await expect(query({ query: communities })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
communities: [
|
communities: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
|
|||||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||||
import { TransactionType } from '../enum/TransactionType'
|
import { TransactionType } from '../enum/TransactionType'
|
||||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||||
|
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
# Test
|
# Test
|
||||||
@ -451,15 +452,15 @@ async function addUserTransaction(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPublicKey(email: string, sessionId: number): Promise<string | undefined> {
|
async function getPublicKey(email: string): Promise<string | null> {
|
||||||
const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', {
|
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||||
session_id: sessionId,
|
const loginUser = await loginUserRepository.findOne({ email: email })
|
||||||
email,
|
// User not found
|
||||||
ask: ['user.pubkeyhex'],
|
if (!loginUser) {
|
||||||
})
|
return null
|
||||||
if (result.success) {
|
|
||||||
return result.data.userData.pubkeyhex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loginUser.pubKey.toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -519,7 +520,7 @@ export class TransactionResolver {
|
|||||||
|
|
||||||
// validate recipient user
|
// validate recipient user
|
||||||
// TODO: the detour over the public key is unnecessary
|
// TODO: the detour over the public key is unnecessary
|
||||||
const recipiantPublicKey = await getPublicKey(email, context.sessionId)
|
const recipiantPublicKey = await getPublicKey(email)
|
||||||
if (!recipiantPublicKey) {
|
if (!recipiantPublicKey) {
|
||||||
throw new Error('recipiant not known')
|
throw new Error('recipiant not known')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { getConnection, getCustomRepository } from 'typeorm'
|
|||||||
import CONFIG from '../../config'
|
import CONFIG from '../../config'
|
||||||
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
|
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
|
||||||
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
|
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
|
||||||
import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse'
|
|
||||||
import { User } from '../model/User'
|
import { User } from '../model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
import encode from '../../jwt/encode'
|
import encode from '../../jwt/encode'
|
||||||
@ -31,6 +30,7 @@ import { LoginUserBackup } from '@entity/LoginUserBackup'
|
|||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { sendEMail } from '../../util/sendEMail'
|
import { sendEMail } from '../../util/sendEMail'
|
||||||
import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
|
import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
|
||||||
|
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
@ -175,7 +175,7 @@ const getEmailHash = (email: string): Buffer => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
const encrypted = Buffer.alloc(sodium.crypto_secretbox_MACBYTES + message.length)
|
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
|
||||||
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
nonce.fill(31) // static nonce
|
nonce.fill(31) // static nonce
|
||||||
|
|
||||||
@ -183,6 +183,16 @@ const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): B
|
|||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
|
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
|
||||||
|
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
nonce.fill(31) // static nonce
|
||||||
|
|
||||||
|
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
@Query(() => User)
|
@Query(() => User)
|
||||||
@ -239,8 +249,12 @@ export class UserResolver {
|
|||||||
user.hasElopage = await this.hasElopage({ pubKey: loginUser.pubKey })
|
user.hasElopage = await this.hasElopage({ pubKey: loginUser.pubKey })
|
||||||
if (!user.hasElopage && publisherId) {
|
if (!user.hasElopage && publisherId) {
|
||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
// TODO: Merge login_call_updateUserInfos
|
// TODO: Check if we can use updateUserInfos
|
||||||
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
|
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
|
||||||
|
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||||
|
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||||
|
loginUser.publisherId = publisherId
|
||||||
|
loginUserRepository.save(loginUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// coinAnimation
|
// coinAnimation
|
||||||
@ -277,13 +291,13 @@ export class UserResolver {
|
|||||||
|
|
||||||
@Authorized()
|
@Authorized()
|
||||||
@Query(() => String)
|
@Query(() => String)
|
||||||
async logout(@Ctx() context: any): Promise<string> {
|
async logout(): Promise<boolean> {
|
||||||
const payload = { session_id: context.sessionId }
|
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
|
||||||
const result = await apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
|
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
|
||||||
if (!result.success) {
|
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
|
||||||
throw new Error(result.data)
|
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
|
||||||
}
|
// we should just return true for now.
|
||||||
return 'success'
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => String)
|
@Mutation(() => String)
|
||||||
@ -460,7 +474,7 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Authorized()
|
@Authorized()
|
||||||
@Mutation(() => UpdateUserInfosResponse)
|
@Mutation(() => Boolean)
|
||||||
async updateUserInfos(
|
async updateUserInfos(
|
||||||
@Args()
|
@Args()
|
||||||
{
|
{
|
||||||
@ -475,80 +489,97 @@ export class UserResolver {
|
|||||||
coinanimation,
|
coinanimation,
|
||||||
}: UpdateUserInfosArgs,
|
}: UpdateUserInfosArgs,
|
||||||
@Ctx() context: any,
|
@Ctx() context: any,
|
||||||
): Promise<UpdateUserInfosResponse> {
|
): Promise<boolean> {
|
||||||
const payload = {
|
|
||||||
session_id: context.sessionId,
|
|
||||||
update: {
|
|
||||||
'User.first_name': firstName || undefined,
|
|
||||||
'User.last_name': lastName || undefined,
|
|
||||||
'User.description': description || undefined,
|
|
||||||
'User.username': username || undefined,
|
|
||||||
'User.language': language || undefined,
|
|
||||||
'User.publisher_id': publisherId || undefined,
|
|
||||||
'User.password': passwordNew || undefined,
|
|
||||||
'User.password_old': password || undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
let response: UpdateUserInfosResponse | undefined
|
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
|
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||||
|
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||||
|
|
||||||
if (
|
|
||||||
firstName ||
|
|
||||||
lastName ||
|
|
||||||
description ||
|
|
||||||
username ||
|
|
||||||
language ||
|
|
||||||
publisherId ||
|
|
||||||
passwordNew ||
|
|
||||||
password
|
|
||||||
) {
|
|
||||||
const result = await apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
|
|
||||||
if (!result.success) throw new Error(result.data)
|
|
||||||
response = new UpdateUserInfosResponse(result.data)
|
|
||||||
|
|
||||||
const pubKeyString = Buffer.from(context.pubKey).toString('hex')
|
|
||||||
const userEntity = await userRepository.findByPubkeyHex(pubKeyString)
|
|
||||||
let userEntityChanged = false
|
|
||||||
if (firstName) {
|
|
||||||
userEntity.firstName = firstName
|
|
||||||
userEntityChanged = true
|
|
||||||
}
|
|
||||||
if (lastName) {
|
|
||||||
userEntity.lastName = lastName
|
|
||||||
userEntityChanged = true
|
|
||||||
}
|
|
||||||
if (username) {
|
if (username) {
|
||||||
userEntity.username = username
|
throw new Error('change username currently not supported!')
|
||||||
userEntityChanged = true
|
// TODO: this error was thrown on login_server whenever you tried to change the username
|
||||||
|
// to anything except "" which is an exception to the rules below. Those were defined
|
||||||
|
// aswell, even tho never used.
|
||||||
|
// ^[a-zA-Z][a-zA-Z0-9_-]*$
|
||||||
|
// username must start with [a-z] or [A-Z] and than can contain also [0-9], - and _
|
||||||
|
// username already used
|
||||||
|
// userEntity.username = username
|
||||||
}
|
}
|
||||||
if (userEntityChanged) {
|
|
||||||
userRepository.save(userEntity).catch((error) => {
|
|
||||||
throw new Error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (coinanimation !== undefined) {
|
|
||||||
// load user and balance
|
|
||||||
const pubKeyString = Buffer.from(context.pubKey).toString('hex')
|
|
||||||
const userEntity = await userRepository.findByPubkeyHex(pubKeyString)
|
|
||||||
|
|
||||||
const userSettingRepository = getCustomRepository(UserSettingRepository)
|
if (firstName) {
|
||||||
userSettingRepository
|
loginUser.firstName = firstName
|
||||||
|
userEntity.firstName = firstName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastName) {
|
||||||
|
loginUser.lastName = lastName
|
||||||
|
userEntity.lastName = lastName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
loginUser.description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
if (language) {
|
||||||
|
if (!isLanguage(language)) {
|
||||||
|
throw new Error(`"${language}" isn't a valid language`)
|
||||||
|
}
|
||||||
|
loginUser.language = language
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password && passwordNew) {
|
||||||
|
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||||
|
const oldPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, password)
|
||||||
|
if (loginUser.password !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||||
|
throw new Error(`Old password is invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const privKey = SecretKeyCryptographyDecrypt(loginUser.privKey, oldPasswordHash[1])
|
||||||
|
|
||||||
|
const newPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, passwordNew) // return short and long hash
|
||||||
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||||
|
|
||||||
|
// Save new password hash and newly encrypted private key
|
||||||
|
loginUser.password = newPasswordHash[0].readBigInt64LE()
|
||||||
|
loginUser.privKey = encryptedPrivkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save publisherId only if Elopage is not yet registered
|
||||||
|
if (publisherId && !(await this.hasElopage(context))) {
|
||||||
|
loginUser.publisherId = publisherId
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (coinanimation) {
|
||||||
|
queryRunner.manager
|
||||||
|
.getCustomRepository(UserSettingRepository)
|
||||||
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
|
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(error)
|
throw new Error('error saving coinanimation: ' + error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.manager.save(loginUser).catch((error) => {
|
||||||
|
throw new Error('error saving loginUser: ' + error)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response) {
|
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||||
response = new UpdateUserInfosResponse({ valid_values: 1 })
|
throw new Error('error saving user: ' + error)
|
||||||
} else {
|
})
|
||||||
response.validValues++
|
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
|
} catch (e) {
|
||||||
|
await queryRunner.rollbackTransaction()
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!response) {
|
return true
|
||||||
throw new Error('no valid response')
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import { createTransport } from 'nodemailer'
|
|||||||
|
|
||||||
import CONFIG from '../config'
|
import CONFIG from '../config'
|
||||||
|
|
||||||
export const sendEMail = async (emailDef: any): Promise<boolean> => {
|
export const sendEMail = async (emailDef: {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
subject: string
|
||||||
|
text: string
|
||||||
|
}): Promise<boolean> => {
|
||||||
if (!CONFIG.EMAIL) {
|
if (!CONFIG.EMAIL) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('Emails are disabled via config')
|
console.log('Emails are disabled via config')
|
||||||
|
|||||||
@ -38,9 +38,7 @@ export const updateUserInfos = gql`
|
|||||||
passwordNew: $passwordNew
|
passwordNew: $passwordNew
|
||||||
language: $locale
|
language: $locale
|
||||||
coinanimation: $coinanimation
|
coinanimation: $coinanimation
|
||||||
) {
|
)
|
||||||
validValues
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
29
frontend/src/plugins/globalComponents.test.js
Normal file
29
frontend/src/plugins/globalComponents.test.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import GlobalComponents from './globalComponents.js'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import 'vee-validate'
|
||||||
|
|
||||||
|
jest.mock('vue')
|
||||||
|
jest.mock('vee-validate', () => {
|
||||||
|
const originalModule = jest.requireActual('vee-validate')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
ValidationProvider: 'mocked validation provider',
|
||||||
|
ValidationObserver: 'mocked validation observer',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const vueComponentMock = jest.fn()
|
||||||
|
Vue.component = vueComponentMock
|
||||||
|
|
||||||
|
describe('global Components', () => {
|
||||||
|
GlobalComponents.install(Vue)
|
||||||
|
|
||||||
|
it('installs the validation provider', () => {
|
||||||
|
expect(vueComponentMock).toBeCalledWith('validation-provider', 'mocked validation provider')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('installs the validation observer', () => {
|
||||||
|
expect(vueComponentMock).toBeCalledWith('validation-observer', 'mocked validation observer')
|
||||||
|
})
|
||||||
|
})
|
||||||
70
frontend/src/views/Pages/SendOverview/GddSend/QrCode.spec.js
Normal file
70
frontend/src/views/Pages/SendOverview/GddSend/QrCode.spec.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import QrCode from './QrCode'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('QrCode', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
}
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
QrcodeStream: true,
|
||||||
|
QrcodeCapture: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(QrCode, { localVue, mocks, stubs })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component', () => {
|
||||||
|
expect(wrapper.find('div.alert').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('scanning', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper.find('a').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a scanning stream', () => {
|
||||||
|
expect(wrapper.findComponent({ name: 'QrcodeStream' }).exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('decode', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper
|
||||||
|
.findComponent({ name: 'QrcodeStream' })
|
||||||
|
.vm.$emit('decode', '[{"email": "user@example.org", "amount": 10.0}]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits set transaction', () => {
|
||||||
|
expect(wrapper.emitted()['set-transaction']).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
email: 'user@example.org',
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.find('div.row > *').vm.$emit('detect')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onDetect', () => {
|
||||||
|
expect(wrapper.vm.detect).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -44,6 +44,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
scan: false,
|
scan: false,
|
||||||
|
detect: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -55,6 +56,10 @@ export default {
|
|||||||
this.$emit('set-transaction', { email: arr[0].email, amount: arr[0].amount })
|
this.$emit('set-transaction', { email: arr[0].email, amount: arr[0].amount })
|
||||||
this.scan = false
|
this.scan = false
|
||||||
},
|
},
|
||||||
|
async onDetect() {
|
||||||
|
// TODO: what is this for? I added the detect data to test that the method is called
|
||||||
|
this.detect = !this.detect
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user