Merge branch 'master' into add-referrer-id-to-users

This commit is contained in:
Hannes Heine 2022-03-22 09:50:09 +01:00 committed by GitHub
commit 2457371300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 819 additions and 141 deletions

View File

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

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query } from 'type-graphql'
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
import { TransactionLink } from '@model/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as dbUser } from '@entity/User'
@ -70,7 +70,10 @@ export class TransactionLinkResolver {
@Authorized([RIGHTS.DELETE_TRANSACTION_LINK])
@Mutation(() => Boolean)
async deleteTransactionLink(@Arg('id') id: number, @Ctx() context: any): Promise<boolean> {
async deleteTransactionLink(
@Arg('id', () => Int) id: number,
@Ctx() context: any,
): Promise<boolean> {
const { user } = context
const transactionLink = await dbTransactionLink.findOne({ id })
@ -96,7 +99,7 @@ export class TransactionLinkResolver {
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
@Query(() => TransactionLink)
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
let redeemedBy: User | null = null
if (transactionLink && transactionLink.redeemedBy) {
@ -131,9 +134,12 @@ export class TransactionLinkResolver {
@Authorized([RIGHTS.REDEEM_TRANSACTION_LINK])
@Mutation(() => Boolean)
async redeemTransactionLink(@Arg('id') id: number, @Ctx() context: any): Promise<boolean> {
async redeemTransactionLink(
@Arg('code', () => String) code: string,
@Ctx() context: any,
): Promise<boolean> {
const { user } = context
const transactionLink = await dbTransactionLink.findOneOrFail({ id })
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
const now = new Date()

View File

@ -19,7 +19,7 @@
<div class="mt-4" v-show="selected === sendTypes.link">
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
<div>
{{ $t('gdd_per_link.sentence_1') }}
{{ $t('gdd_per_link.choose-amount') }}
</div>
</div>

View File

@ -0,0 +1,21 @@
<template>
<div class="redeem-information">
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
<h1>
{{ firstName }}
{{ $t('transaction-link.send_you') }} {{ amount | GDD }}
</h1>
<b>{{ memo }}</b>
</b-jumbotron>
</div>
</template>
<script>
export default {
name: 'RedeemInformation',
props: {
firstName: { type: String, required: true },
amount: { type: String, required: true },
memo: { type: String, required: true, default: '' },
},
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="redeem-logged-out">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
<b-jumbotron>
<div class="mb-6">
<h2>{{ $t('gdd_per_link.redeem') }}</h2>
</div>
<b-row>
<b-col col sm="12" md="6">
<p>{{ $t('gdd_per_link.no-account') }}</p>
<b-button variant="primary" :to="register">
{{ $t('gdd_per_link.to-register') }}
</b-button>
</b-col>
<b-col sm="12" md="6" class="mt-xs-6 mt-sm-6 mt-md-0">
<p>{{ $t('gdd_per_link.has-account') }}</p>
<b-button variant="info" :to="login">{{ $t('gdd_per_link.to-login') }}</b-button>
</b-col>
</b-row>
</b-jumbotron>
</div>
</template>
<script>
import RedeemInformation from '@/components/LinkInformations/RedeemInformation.vue'
export default {
name: 'RedeemLoggedOut',
components: {
RedeemInformation,
},
props: {
user: { type: Object, required: true },
amount: { type: String, required: true },
memo: { type: String, required: true, default: '' },
},
computed: {
login() {
return '/login/' + this.$route.params.code
},
register() {
return '/register/' + this.$route.params.code
},
},
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="redeem-self-creator">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
<b-jumbotron>
<div class="mb-3 text-center">
<div class="mt-3">
{{ $t('gdd_per_link.no-redeem') }}
<a to="/transactions" href="#!">
<b>{{ $t('gdd_per_link.link-overview') }}</b>
</a>
</div>
</div>
</b-jumbotron>
</div>
</template>
<script>
import RedeemInformation from '@/components/LinkInformations/RedeemInformation.vue'
export default {
name: 'RedeemSelfCreator',
components: {
RedeemInformation,
},
props: {
user: { type: Object, required: true },
amount: { type: String, required: true },
memo: { type: String, required: true, default: '' },
},
}
</script>

View File

@ -0,0 +1,27 @@
<template>
<div class="redeem-valid">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
<b-jumbotron>
<div class="mb-3 text-center">
<b-button variant="primary" @click="$emit('redeem-link', amount)" size="lg">
{{ $t('gdd_per_link.redeem') }}
</b-button>
</div>
</b-jumbotron>
</div>
</template>
<script>
import RedeemInformation from '@/components/LinkInformations/RedeemInformation.vue'
export default {
name: 'RedeemValid',
components: {
RedeemInformation,
},
props: {
user: { type: Object, required: false },
amount: { type: String, required: false },
memo: { type: String, required: false, default: '' },
},
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="redeemed-text-box">
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
<h1>
{{ text }}
</h1>
</b-jumbotron>
<div class="text-center">
<b-button to="/overview">{{ $t('back') }}</b-button>
</div>
</div>
</template>
<script>
export default {
name: 'RedeemedTextBox',
props: {
text: { type: String, required: true },
},
}
</script>

View File

@ -0,0 +1,16 @@
<template>
<div>
<slot :name="type"></slot>
</div>
</template>
<script>
export default {
name: 'TransactionLinkItem',
props: {
type: {
type: String,
required: true,
},
},
}
</script>

View File

@ -5,7 +5,7 @@
</template>
<script>
export default {
name: 'TransactionList',
name: 'TransactionListItem',
props: {
typeId: {
type: String,

View File

@ -73,7 +73,13 @@ export const createTransactionLink = gql`
`
export const deleteTransactionLink = gql`
mutation($id: Float!) {
mutation($id: Int!) {
deleteTransactionLink(id: $id)
}
`
export const redeemTransactionLink = gql`
mutation($code: String!) {
redeemTransactionLink(code: $code)
}
`

View File

@ -128,13 +128,17 @@ export const queryOptIn = gql`
export const queryTransactionLink = gql`
query($code: String!) {
queryTransactionLink(code: $code) {
id
amount
memo
createdAt
validUntil
redeemedAt
deletedAt
user {
firstName
publisherId
email
}
}
}

View File

@ -94,18 +94,30 @@
},
"GDD": "GDD",
"gdd_per_link": {
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
"copy": "kopieren",
"created": "Der Link wurde erstellt!",
"decay-14-day": "Vergänglichkeit für 14 Tage",
"delete-the-link": "Den Link löschen?",
"deleted": "Der Link wurde gelöscht!",
"expired": "Abgelaufen",
"has-account": "Du besitzt bereits ein Gradido Konto",
"header": "Gradidos versenden per Link",
"link-copied": "Link wurde in die Zwischenablage kopiert",
"link-deleted": "Der Link wurde am {date} gelöscht.",
"link-expired": "Der Link ist nicht mehr gültig. Die Gültigkeit ist am {date} abgelaufen.",
"link-overview": "Linkübersicht",
"links_count": "Aktive Links",
"links_sum": "Summe deiner versendeten Gradidos",
"no-account": "Du besitzt noch kein Gradido Konto",
"no-redeem": "Du darfst deinen eigenen Link nicht einlösen!",
"not-copied": "Konnte den Link nicht kopieren: {err}",
"sentence_1": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „jetzt generieren“ wird ein Link erstellt, den du versenden kannst."
"redeem": "Einlösen",
"redeem-text": "Willst du den Betrag jetzt einlösen?",
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
"to-login": "Log dich ein",
"to-register": "Registriere ein neues Konto"
},
"gdt": {
"calculation": "Berechnung der GradidoTransform",
@ -236,7 +248,6 @@
"show_all": "Alle <strong>{count}</strong> Transaktionen ansehen"
},
"transaction-link": {
"button": "einlösen",
"send_you": "sendet dir"
}
}

View File

@ -94,18 +94,30 @@
},
"GDD": "GDD",
"gdd_per_link": {
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
"copy": "copy",
"created": "Link was created!",
"decay-14-day": "Decay for 14 days",
"delete-the-link": "Delete the link?",
"deleted": "The link was deleted!",
"expired": "Expired",
"has-account": "You already have a Gradido account",
"header": "Send Gradidos via link",
"link-copied": "Link copied to clipboard",
"link-deleted": "The link was deleted on {date}.",
"link-expired": "The link is no longer valid. The validity expired on {date}.",
"link-overview": "Link overview",
"links_count": "Active links",
"links_sum": "Total of your sent Gradidos",
"no-account": "You don't have a Gradido account yet",
"no-redeem": "You not allowed to redeem your own link!",
"not-copied": "Could not copy link: {err}",
"sentence_1": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share."
"redeem": "Redeem",
"redeem-text": "Do you want to redeem the amount now?",
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
"redeemed-at": "The link was already redeemed on {date}.",
"to-login": "Log in",
"to-register": "Register a new account"
},
"gdt": {
"calculation": "Calculation of GradidoTransform",
@ -236,7 +248,6 @@
"show_all": "View all <strong>{count}</strong> transactions."
},
"transaction-link": {
"button": "redeem",
"send_you": "wants to send you"
}
}

View File

@ -52,6 +52,9 @@ describe('Login', () => {
$router: {
push: mockRouterPush,
},
$route: {
params: {},
},
$apollo: {
query: apolloQueryMock,
},
@ -224,13 +227,33 @@ describe('Login', () => {
expect(mockStoreDispach).toBeCalledWith('login', 'token')
})
it('redirects to overview page', () => {
expect(mockRouterPush).toBeCalledWith('/overview')
})
it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled()
})
describe('without code parameter', () => {
it('redirects to overview page', () => {
expect(mockRouterPush).toBeCalledWith('/overview')
})
})
describe('with code parameter', () => {
beforeEach(async () => {
mocks.$route.params = {
code: 'some-code',
}
wrapper = Wrapper()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('redirects to overview page', () => {
expect(mockRouterPush).toBeCalledWith('/redeem/some-code')
})
})
})
describe('login fails', () => {

View File

@ -96,13 +96,17 @@ export default {
},
fetchPolicy: 'network-only',
})
.then((result) => {
.then(async (result) => {
const {
data: { login },
} = result
this.$store.dispatch('login', login)
this.$router.push('/overview')
loader.hide()
await loader.hide()
if (this.$route.params.code) {
this.$router.push(`/redeem/${this.$route.params.code}`)
} else {
this.$router.push('/overview')
}
})
.catch((error) => {
this.toastError(this.$t('error.no-account'))

View File

@ -4,7 +4,7 @@
<div class="header py-1 py-lg-1 pt-lg-3">
<b-container>
<div class="header-body text-center mb-3">
<a href="#!" @click="goback">
<a href="#!" v-on:click="$router.go(-1)">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-12 mt-5 mb-5">
@ -1186,7 +1186,7 @@
</b-container>
</div>
<div class="text-center">
<b-button class="test-back" variant="light" @click="goback">
<b-button class="test-back" variant="light" v-on:click="$router.go(-1)">
{{ $t('back') }}
</b-button>
</div>
@ -1218,11 +1218,6 @@ export default {
},
}
},
methods: {
goback() {
this.$router.go(-1)
},
},
}
</script>
<style>

View File

@ -32,6 +32,9 @@ describe('Register', () => {
$router: {
push: routerPushMock,
},
$route: {
params: {},
},
$apollo: {
mutate: registerUserMutationMock,
query: apolloQueryMock,
@ -312,6 +315,45 @@ describe('Register', () => {
})
})
})
// TODO: line 157
describe('redeem code', () => {
describe('no redeem code', () => {
it('has no redeem code', () => {
expect(wrapper.vm.redeemCode).toBe(undefined)
})
})
})
describe('with redeem code', () => {
beforeEach(async () => {
jest.clearAllMocks()
mocks.$route.params = {
code: 'some-code',
}
wrapper = Wrapper()
wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
wrapper.find('#registerCheckbox').setChecked()
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('sends the redeem code to the server', () => {
expect(registerUserMutationMock).toBeCalledWith(
expect.objectContaining({
variables: {
email: 'max.mustermann@gradido.net',
firstName: 'Max',
lastName: 'Mustermann',
language: 'en',
publisherId: 12345,
redeemCode: 'some-code',
},
}),
)
})
})
})
})

View File

@ -118,12 +118,14 @@
{{ messageError }}
</span>
</b-alert>
<b-row v-b-toggle:my-collapse class="text-muted shadow-sm p-3 publisherCollaps">
<b-col>{{ $t('publisher.publisherId') }} {{ $store.state.publisherId }}</b-col>
<b-col class="text-right">
<b-icon icon="chevron-down" aria-hidden="true"></b-icon>
</b-col>
</b-row>
<b-row>
<b-col>
<b-collapse id="my-collapse" class="">
@ -136,7 +138,7 @@
type="text"
placeholder="Publisher ID"
v-model="publisherId"
@input="commitStore(publisherId)"
@input="commitStorePublisherId(publisherId)"
></b-form-input>
</b-input-group>
<div
@ -210,6 +212,7 @@ export default {
messageError: '',
register: true,
publisherId: this.$store.state.publisherId,
redeemCode: this.$route.params.code,
}
},
methods: {
@ -220,7 +223,7 @@ export default {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
commitStore(val) {
commitStorePublisherId(val) {
this.$store.commit('publisherId', val)
},
async onSubmit() {
@ -233,6 +236,7 @@ export default {
lastName: this.form.lastname,
language: this.language,
publisherId: this.$store.state.publisherId,
redeemCode: this.redeemCode,
},
})
.then(() => {

View File

@ -1,49 +0,0 @@
import { mount } from '@vue/test-utils'
import ShowTransactionLinkInformations from './ShowTransactionLinkInformations'
const localVue = global.localVue
const errorHandler = jest.fn()
localVue.config.errorHandler = errorHandler
const queryTransactionLink = jest.fn()
queryTransactionLink.mockResolvedValue('success')
const createMockObject = (code) => {
return {
localVue,
mocks: {
$t: jest.fn((t) => t),
$i18n: {
locale: () => 'en',
},
$apollo: {
query: queryTransactionLink,
},
$route: {
params: {
code,
},
},
},
}
}
describe('ShowTransactionLinkInformations', () => {
let wrapper
const Wrapper = (functionN) => {
return mount(ShowTransactionLinkInformations, functionN)
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper(createMockObject())
})
it('renders the component', () => {
expect(wrapper.find('div.show-transaction-link-informations').exists()).toBeTruthy()
})
})
})

View File

@ -1,57 +0,0 @@
<template>
<div class="show-transaction-link-informations">
<!-- Header -->
<div class="header py-7 py-lg-8 pt-lg-9">
<b-container>
<div class="header-body text-center mb-7">
<p class="h1">
{{ displaySetup.user.firstName }}
{{ $t('transaction-link.send_you') }} {{ displaySetup.amount | GDD }}
</p>
<p class="h4">{{ displaySetup.memo }}</p>
<hr />
<b-button v-if="displaySetup.linkTo" :to="displaySetup.linkTo">
{{ $t('transaction-link.button') }}
</b-button>
</div>
</b-container>
</div>
</div>
</template>
<script>
import { queryTransactionLink } from '@/graphql/queries'
export default {
name: 'ShowTransactionLinkInformations',
data() {
return {
displaySetup: {
user: {
firstName: '',
},
},
}
},
methods: {
setTransactionLinkInformation() {
this.$apollo
.query({
query: queryTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then((result) => {
this.displaySetup = result.data.queryTransactionLink
this.$store.commit('publisherId', result.data.queryTransactionLink.user.publisherId)
})
.catch((error) => {
this.toastError(error)
})
},
},
created() {
this.setTransactionLinkInformation()
},
}
</script>

View File

@ -0,0 +1,360 @@
import { mount } from '@vue/test-utils'
import TransactionLink from './TransactionLink'
import { queryTransactionLink } from '@/graphql/queries'
import { redeemTransactionLink } from '@/graphql/mutations'
import { toastSuccessSpy, toastErrorSpy } from '@test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn()
const apolloMutateMock = jest.fn()
const routerPushMock = jest.fn()
const now = new Date().toISOString()
const transactionLinkValidExpireDate = () => {
const validUntil = new Date()
return new Date(validUntil.setDate(new Date().getDate() + 14)).toISOString()
}
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: '2022-03-18T10:08:43.000Z',
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
const mocks = {
$t: jest.fn((t, obj = null) => (obj ? [t, obj.date].join('; ') : t)),
$store: {
state: {
token: null,
email: 'bibi@bloxberg.de',
},
},
$apollo: {
query: apolloQueryMock,
mutate: apolloMutateMock,
},
$route: {
params: {
code: 'some-code',
},
},
$router: {
push: routerPushMock,
},
}
describe('TransactionLink', () => {
let wrapper
const Wrapper = () => {
return mount(TransactionLink, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.show-transaction-link-informations').exists()).toBe(true)
})
it('calls the queryTransactionLink query', () => {
expect(apolloQueryMock).toBeCalledWith({
query: queryTransactionLink,
variables: {
code: 'some-code',
},
})
})
describe('deleted link', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: '2022-03-18T10:08:43.000Z',
deletedAt: now,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
})
it('has a component RedeemedTextBox', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true)
})
it('has a link deleted text in text box', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain(
'gdd_per_link.link-deleted; ' + now,
)
})
})
describe('expired link', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: '2020-03-18T10:08:43.000Z',
redeemedAt: '2022-03-18T10:08:43.000Z',
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
})
it('has a component RedeemedTextBox', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true)
})
it('has a link deleted text in text box', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain(
'gdd_per_link.link-expired; 2020-03-18T10:08:43.000Z',
)
})
})
describe('redeemed link', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: '2022-03-18T10:08:43.000Z',
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
})
it('has a component RedeemedTextBox', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).exists()).toBe(true)
})
it('has a link deleted text in text box', () => {
expect(wrapper.findComponent({ name: 'RedeemedTextBox' }).text()).toContain(
'gdd_per_link.redeemed-at; 2022-03-18T10:08:43.000Z',
)
})
})
describe('no token in store', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
})
it('has a RedeemLoggedOut component', () => {
expect(wrapper.findComponent({ name: 'RedeemLoggedOut' }).exists()).toBe(true)
})
it('has a link to register with code', () => {
expect(wrapper.find('a[href="/register/some-code"]').exists()).toBe(true)
})
it('has a link to login with code', () => {
expect(wrapper.find('a[href="/login/some-code"]').exists()).toBe(true)
})
})
describe('token in store and own link', () => {
beforeEach(() => {
mocks.$store.state.token = 'token'
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' },
},
},
})
wrapper = Wrapper()
})
it('has a RedeemSelfCreator component', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true)
})
it('has a no redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain(
'gdd_per_link.no-redeem',
)
})
it('has a link to transaction page', () => {
expect(wrapper.find('a[to="/transactions"]').exists()).toBe(true)
})
})
describe('valid link', () => {
beforeEach(() => {
mocks.$store.state.token = 'token'
apolloQueryMock.mockResolvedValue({
data: {
queryTransactionLink: {
id: 92,
amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
createdAt: '2022-03-17T16:10:28.000Z',
validUntil: transactionLinkValidExpireDate(),
redeemedAt: null,
deletedAt: null,
user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' },
},
},
})
wrapper = Wrapper()
})
it('has a RedeemValid component', () => {
expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true)
})
it('has a button with redeem text', () => {
expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe(
'gdd_per_link.redeem',
)
})
describe('redeem link with success', () => {
let spy
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockResolvedValue()
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
})
it('opens the modal', () => {
expect(spy).toBeCalledWith('gdd_per_link.redeem-text')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: redeemTransactionLink,
variables: {
code: 'some-code',
},
}),
)
})
it('toasts a success message', () => {
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeemed', { n: '22' })
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
})
it('pushes the route to overview', () => {
expect(routerPushMock).toBeCalledWith('/overview')
})
})
describe('cancel redeem link', () => {
let spy
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockResolvedValue()
spy.mockImplementation(() => Promise.resolve(false))
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
})
it('does not call the API', () => {
expect(apolloMutateMock).not.toBeCalled()
})
it('does not toasts a success message', () => {
expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' })
expect(toastSuccessSpy).not.toBeCalled()
})
it('does not push the route', () => {
expect(routerPushMock).not.toBeCalled()
})
})
describe('redeem link with error', () => {
let spy
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' })
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh Noo!')
})
it('pushes the route to overview', () => {
expect(routerPushMock).toBeCalledWith('/overview')
})
})
})
describe('error on transaction link query', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'Ouchh!' })
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouchh!')
})
})
})
})

View File

@ -0,0 +1,148 @@
<template>
<div class="show-transaction-link-informations">
<div class="text-center"><b-img :src="img" fluid alt="logo"></b-img></div>
<b-container class="mt-4">
<transaction-link-item :type="itemType">
<template #LOGGED_OUT>
<redeem-logged-out v-bind="linkData" />
</template>
<template #SELF_CREATOR>
<redeem-self-creator v-bind="linkData" />
</template>
<template #VALID>
<redeem-valid v-bind="linkData" @redeem-link="redeemLink" />
</template>
<template #TEXT>
<redeemed-text-box :text="redeemedBoxText" />
</template>
</transaction-link-item>
</b-container>
</div>
</template>
<script>
import TransactionLinkItem from '@/components/TransactionLinkItem'
import RedeemLoggedOut from '@/components/LinkInformations/RedeemLoggedOut'
import RedeemSelfCreator from '@/components/LinkInformations/RedeemSelfCreator'
import RedeemValid from '@/components/LinkInformations/RedeemValid'
import RedeemedTextBox from '@/components/LinkInformations/RedeemedTextBox'
import { queryTransactionLink } from '@/graphql/queries'
import { redeemTransactionLink } from '@/graphql/mutations'
export default {
name: 'TransactionLink',
components: {
TransactionLinkItem,
RedeemLoggedOut,
RedeemSelfCreator,
RedeemValid,
RedeemedTextBox,
},
data() {
return {
img: '/img/brand/green.png',
linkData: {
amount: '123.45',
memo: 'memo',
user: {
firstName: 'Bibi',
},
deletedAt: null,
},
}
},
methods: {
setTransactionLinkInformation() {
this.$apollo
.query({
query: queryTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then((result) => {
this.linkData = result.data.queryTransactionLink
})
.catch((err) => {
this.toastError(err.message)
})
},
redeemLink(amount) {
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then(async (value) => {
if (value)
await this.$apollo
.mutate({
mutation: redeemTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then(() => {
this.toastSuccess(
this.$t('gdd_per_link.redeemed', {
n: amount,
}),
)
this.$router.push('/overview')
})
.catch((err) => {
this.toastError(err.message)
this.$router.push('/overview')
})
})
},
},
computed: {
itemType() {
// link wurde gelöscht: am, von
if (this.linkData.deletedAt) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.redeemedBoxText = this.$t('gdd_per_link.link-deleted', {
date: this.linkData.deletedAt,
})
return `TEXT`
}
// link ist abgelaufen, nicht gelöscht
if (new Date(this.linkData.validUntil) < new Date()) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.redeemedBoxText = this.$t('gdd_per_link.link-expired', {
date: this.linkData.validUntil,
})
return `TEXT`
}
// der link wurde eingelöst, nicht gelöscht
if (this.linkData.redeemedAt) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.redeemedBoxText = this.$t('gdd_per_link.redeemed-at', {
date: this.linkData.redeemedAt,
})
return `TEXT`
}
if (this.$store.state.token) {
// logged in, nicht berechtigt einzulösen, eigener link
if (this.$store.state.email === this.linkData.user.email) {
return `SELF_CREATOR`
}
// logged in und berechtigt einzulösen
if (
this.$store.state.email !== this.linkData.user.email &&
!this.linkData.redeemedAt &&
!this.linkData.deletedAt
) {
return `VALID`
}
}
return `LOGGED_OUT`
},
},
created() {
this.setTransactionLinkInformation()
},
}
</script>

View File

@ -99,14 +99,14 @@ describe('router', () => {
describe('login', () => {
it('loads the "Login" page', async () => {
const component = await routes.find((r) => r.path === '/login').component()
const component = await routes.find((r) => r.path === '/login/:code?').component()
expect(component.default.name).toBe('Login')
})
})
describe('register', () => {
it('loads the "register" page', async () => {
const component = await routes.find((r) => r.path === '/register').component()
const component = await routes.find((r) => r.path === '/register/:code?').component()
expect(component.default.name).toBe('Register')
})
})
@ -182,6 +182,13 @@ describe('router', () => {
})
})
describe('redeem', () => {
it('loads the "TransactionLink" page', async () => {
const component = await routes.find((r) => r.path === '/redeem/:code').component()
expect(component.default.name).toBe('TransactionLink')
})
})
describe('not found page', () => {
it('renders the "NotFound" page', async () => {
expect(routes.find((r) => r.path === '*').component).toEqual(NotFound)

View File

@ -39,11 +39,11 @@ const routes = [
},
},
{
path: '/login',
path: '/login/:code?',
component: () => import('@/pages/Login.vue'),
},
{
path: '/register',
path: '/register/:code?',
component: () => import('@/pages/Register.vue'),
},
{
@ -84,7 +84,7 @@ const routes = [
},
{
path: '/redeem/:code',
component: () => import('@/pages/ShowTransactionLinkInformations.vue'),
component: () => import('@/pages/TransactionLink.vue'),
},
{ path: '*', component: NotFound },
]