diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3687cc5a..de9939101 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -344,7 +344,7 @@ jobs: report_name: Coverage Frontend type: lcov result_path: ./coverage/lcov.info - min_coverage: 73 + min_coverage: 76 token: ${{ github.token }} ############################################################################## diff --git a/.gitignore b/.gitignore index a5dadd281..caed320af 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ messages.pot nbproject .metadata /.env +package-lock.json diff --git a/backend/.env.dist b/backend/.env.dist index 150269632..3ac50ac9b 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -14,4 +14,8 @@ DB_DATABASE=gradido_community #KLICKTIPP_PASSWORD= #KLICKTIPP_APIKEY_DE= #KLICKTIPP_APIKEY_EN= -#KLICKTIPP=true \ No newline at end of file +#KLICKTIPP=true +COMMUNITY_NAME= +COMMUNITY_URL= +COMMUNITY_REGISTER_URL= +COMMUNITY_DESCRIPTION= \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 4719595bb..4620db4ad 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "graphql": "^15.5.1", "jest": "^27.2.4", "jsonwebtoken": "^8.5.1", + "libsodium-wrappers": "^0.7.9", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "ts-jest": "^27.0.5", @@ -35,6 +36,7 @@ "devDependencies": { "@types/express": "^4.17.12", "@types/jsonwebtoken": "^8.5.2", + "@types/libsodium-wrappers": "^0.7.9", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "eslint": "^7.29.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 98a67864c..76ba597ad 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -30,9 +30,17 @@ const klicktipp = { KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', } +const community = { + COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', + COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/vue/', + COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/vue/register', + COMMUNITY_DESCRIPTION: + process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', +} + // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET -const CONFIG = { ...server, ...database, ...klicktipp } +const CONFIG = { ...server, ...database, ...klicktipp, ...community } export default CONFIG diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts new file mode 100644 index 000000000..466028d00 --- /dev/null +++ b/backend/src/graphql/model/Community.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ObjectType, Field } from 'type-graphql' + +@ObjectType() +export class Community { + constructor(json?: any) { + if (json) { + this.id = Number(json.id) + this.name = json.name + this.url = json.url + this.description = json.description + this.registerUrl = json.registerUrl + } + } + + @Field(() => Number) + id: number + + @Field(() => String) + name: string + + @Field(() => String) + url: string + + @Field(() => String) + description: string + + @Field(() => String) + registerUrl: string +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 21bf1b464..ebdf0aad2 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -19,6 +19,7 @@ export class User { this.pubkey = json.public_hex this.language = json.language this.publisherId = json.publisher_id + if (json.hasElopage) this.hasElopage = json.hasElopage } @Field(() => String) @@ -74,4 +75,7 @@ export class User { @Field(() => KlickTipp) klickTipp: KlickTipp + + @Field(() => Boolean) + hasElopage?: boolean } diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts new file mode 100644 index 000000000..563c73d24 --- /dev/null +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { Resolver, Query } from 'type-graphql' +import CONFIG from '../../config' +import { Community } from '../model/Community' + +@Resolver() +export class CommunityResolver { + @Query(() => Community) + async getCommunityInfo(): Promise { + return new Community({ + name: CONFIG.COMMUNITY_NAME, + description: CONFIG.COMMUNITY_DESCRIPTION, + url: CONFIG.COMMUNITY_URL, + registerUrl: CONFIG.COMMUNITY_REGISTER_URL, + }) + } + + @Query(() => [Community]) + async communities(): Promise { + const communities: Community[] = [] + + communities.push( + new Community({ + id: 1, + name: 'Gradido Entwicklung', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register-community', + }), + new Community({ + id: 2, + name: 'Gradido Staging', + description: 'Der Testserver der Gradido-Akademie.', + url: 'https://stage1.gradido.net/vue/', + registerUrl: 'https://stage1.gradido.net/vue/register-community', + }), + new Community({ + id: 3, + name: 'Gradido-Akademie', + description: 'Freies Institut für Wirtschaftsbionik.', + url: 'https://gradido.net', + registerUrl: 'https://gdd1.gradido.com/vue/register-community', + }), + ) + return communities + } +} diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 9ca9421f6..b4f9a512b 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql' +import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql' import { getCustomRepository } from 'typeorm' import CONFIG from '../../config' import { GdtEntryList } from '../model/GdtEntryList' @@ -32,4 +32,16 @@ export class GdtResolver { } return new GdtEntryList(resultGDT.data) } + + @Authorized() + @Query(() => Number) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async existPid(@Arg('pid') pid: number): Promise { + // load user + const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`) + if (!resultPID.success) { + throw new Error(resultPID.data) + } + return resultPID.data.pid + } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0ba6efb2..ec52bef8b 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -23,7 +23,7 @@ import { User as dbUser } from '../../typeorm/entity/User' import { UserTransaction as dbUserTransaction } from '../../typeorm/entity/UserTransaction' import { Transaction as dbTransaction } from '../../typeorm/entity/Transaction' -import { apiGet, apiPost } from '../../apis/HttpRequest' +import { apiPost } from '../../apis/HttpRequest' import { roundFloorFrom4 } from '../../util/round' import { calculateDecay, calculateDecayWithInterval } from '../../util/decay' import { TransactionTypeId } from '../enum/TransactionTypeId' @@ -216,13 +216,9 @@ export class TransactionResolver { @Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated, @Ctx() context: any, ): Promise { - // get public key for current logged in user - const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId) - if (!result.success) throw new Error(result.data) - // load user const userRepository = getCustomRepository(UserRepository) - const userEntity = await userRepository.findByPubkeyHex(result.data.user.public_hex) + const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const transactions = await listTransactions(currentPage, pageSize, order, userEntity) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 5a2a049de..b2d6d89d9 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -2,12 +2,14 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' +import { from_hex as fromHex } from 'libsodium-wrappers' import CONFIG from '../../config' import { CheckUsernameResponse } from '../model/CheckUsernameResponse' import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode' import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse' import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse' import { User } from '../model/User' +import { User as DbUser } from '../../typeorm/entity/User' import encode from '../../jwt/encode' import ChangePasswordArgs from '../arg/ChangePasswordArgs' import CheckUsernameArgs from '../arg/CheckUsernameArgs' @@ -45,7 +47,22 @@ export class UserResolver { const user = new User(result.data.user) // read additional settings from settings table const userRepository = getCustomRepository(UserRepository) - const userEntity = await userRepository.findByPubkeyHex(user.pubkey) + let userEntity: void | DbUser + userEntity = await userRepository.findByPubkeyHex(user.pubkey).catch(() => { + userEntity = new DbUser() + userEntity.firstName = user.firstName + userEntity.lastName = user.lastName + userEntity.username = user.username + userEntity.email = user.email + userEntity.pubkey = Buffer.from(fromHex(user.pubkey)) + + userEntity.save().catch(() => { + throw new Error('error by save userEntity') + }) + }) + if (!userEntity) { + throw new Error('error with cannot happen') + } const userSettingRepository = getCustomRepository(UserSettingRepository) const coinanimation = await userSettingRepository @@ -102,6 +119,18 @@ export class UserResolver { throw new Error(result.data) } + const user = new User(result.data.user) + const dbuser = new DbUser() + dbuser.pubkey = Buffer.from(fromHex(user.pubkey)) + dbuser.email = user.email + dbuser.firstName = user.firstName + dbuser.lastName = user.lastName + dbuser.username = user.username + + dbuser.save().catch(() => { + throw new Error('error saving user') + }) + return 'success' } @@ -228,4 +257,13 @@ export class UserResolver { } return new CheckEmailResponse(result.data) } + + @Query(() => Boolean) + async hasElopage(@Ctx() context: any): Promise { + const result = await apiGet(CONFIG.LOGIN_API_URL + 'hasElopage?session_id=' + context.sessionId) + if (!result.success) { + throw new Error(result.data) + } + return result.data.hasElopage + } } diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 2a2299a4c..86f7119c4 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -10,6 +10,7 @@ export class UserRepository extends Repository { } async getUsersIndiced(userIds: number[]): Promise { + if (!userIds.length) return [] const users = await this.createQueryBuilder('user') .select(['user.id', 'user.firstName', 'user.lastName', 'user.email']) .where('user.id IN (:...users)', { users: userIds }) diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index 7ea97ada9..81457ab34 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -14,6 +14,8 @@ async function calculateDecay(amount: number, from: Date, to: Date): Promise to + // Do we want to have negative decay? const decayDuration = (to.getTime() - from.getTime()) / 1000 return decayFormula(amount, decayDuration) } diff --git a/backend/yarn.lock b/backend/yarn.lock index 714823fe9..11401ccf0 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -888,6 +888,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/libsodium-wrappers@^0.7.9": + version "0.7.9" + resolved "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz" + integrity sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz" @@ -914,9 +919,9 @@ integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== "@types/node@^14.11.2": - version "14.17.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0" - integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A== + version "14.17.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.21.tgz#6359d8cf73481e312a43886fa50afc70ce5592c6" + integrity sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA== "@types/prettier@^2.1.5": version "2.4.1" @@ -1686,7 +1691,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz" integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== @@ -1694,14 +1699,6 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -3874,6 +3871,18 @@ libphonenumber-js@^1.9.7: resolved "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.22.tgz" integrity sha512-nE0aF0wrNq09ewF36s9FVqRW73hmpw6cobVDlbexmsu1432LEfuN24BCudNuRx4t2rElSeK/N0JbedzRW/TC4A== +libsodium-wrappers@^0.7.9: + version "0.7.9" + resolved "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz" + integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ== + dependencies: + libsodium "^0.7.0" + +libsodium@^0.7.0: + version "0.7.9" + resolved "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz" + integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A== + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" diff --git a/docu/login_server.api.md b/docu/login_server.api.md index 20317c66f..aeac22549 100644 --- a/docu/login_server.api.md +++ b/docu/login_server.api.md @@ -67,6 +67,7 @@ In case of success returns: "username": "" }, "session_id": -127182, + "hasElopage": true, "clientIP":"123.123.123.123" } ``` @@ -86,6 +87,7 @@ In case of success returns: - `role`: role of user currently only "none" or "admin" - `username`: not used yet - `clientIP`: should be the same as where the js-client is running, else maybe a man-in-the-middle attacks is happening or +- `hasElopage`: only present if hasElopage was set to true in request, true if user has an elopage account nginx was wrong configured. - `session_id`: can be also negative @@ -593,3 +595,29 @@ or: "msg": "session not found" } ``` + +## Check if User has an Elopage Account +Check if logged in user has already an elopage account + +### Request +`GET http://localhost/login_api/hasElopage?session_id=-127182` + +### Response +In case of success returns: + +```json +{ + "state":"success", + "hasElopage": true +} +``` + +or: + +```json +{ + "state":"not found", + "msg": "session not found" +} +``` + diff --git a/frontend/src/components/LanguageSwitch.spec.js b/frontend/src/components/LanguageSwitch.spec.js index dc76d854b..cf7c4a35e 100644 --- a/frontend/src/components/LanguageSwitch.spec.js +++ b/frontend/src/components/LanguageSwitch.spec.js @@ -122,7 +122,6 @@ describe('LanguageSwitch', () => { expect(updateUserInfosMutationMock).toBeCalledWith( expect.objectContaining({ variables: { - email: 'he@ho.he', locale: 'en', }, }), @@ -134,7 +133,6 @@ describe('LanguageSwitch', () => { expect(updateUserInfosMutationMock).toBeCalledWith( expect.objectContaining({ variables: { - email: 'he@ho.he', locale: 'de', }, }), diff --git a/frontend/src/components/LanguageSwitch.vue b/frontend/src/components/LanguageSwitch.vue index 14894e46e..5e4c71287 100644 --- a/frontend/src/components/LanguageSwitch.vue +++ b/frontend/src/components/LanguageSwitch.vue @@ -39,7 +39,6 @@ export default { .mutate({ mutation: updateUserInfos, variables: { - email: this.$store.state.email, locale: locale, }, }) diff --git a/frontend/src/components/LanguageSwitchSelect.vue b/frontend/src/components/LanguageSwitchSelect.vue index 3467bdafd..545cef4e9 100644 --- a/frontend/src/components/LanguageSwitchSelect.vue +++ b/frontend/src/components/LanguageSwitchSelect.vue @@ -14,8 +14,8 @@ export default { return { selected: null, options: [ - { value: 'de', text: this.$t('languages.de') }, - { value: 'en', text: this.$t('languages.en') }, + { value: 'de', text: this.$t('settings.language.de') }, + { value: 'en', text: this.$t('settings.language.en') }, ], } }, diff --git a/frontend/src/directives/click-ouside.js b/frontend/src/directives/click-ouside.js.unused similarity index 100% rename from frontend/src/directives/click-ouside.js rename to frontend/src/directives/click-ouside.js.unused diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index fe1c58e1b..3499a3fa1 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -105,3 +105,26 @@ export const checkEmailQuery = gql` } } ` + +export const communityInfo = gql` + query { + getCommunityInfo { + name + description + registerUrl + url + } + } +` + +export const communities = gql` + query { + communities { + id + name + url + description + registerUrl + } + } +` diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 93b70318c..c41609a0d 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -1,14 +1,22 @@ { "back": "Zurück", - "community": "Gemeinschaft", - "communitys": { - "form": { - "date_period": "Datum / Zeitraum", - "hours": "Stunden", - "hours_report": "Stundenbericht", - "more_hours": "weitere Stunden", - "submit": "Einreichen" - } + "community": { + "choose-another-community": "Eine andere Gemeinschaft auswählen", + "communities": { + "form": { + "date_period": "Datum / Zeitraum", + "hours": "Stunden", + "hours_report": "Stundenbericht", + "more_hours": "weitere Stunden", + "submit": "Einreichen" + } + }, + "community": "Gemeinschaft", + "continue-to-registration": "Weiter zur Registrierung", + "current-community": "Aktuelle Gemeinschaft", + "location": "Ort:", + "other-communities": "Weitere Gemeinschaften", + "switch-to-this-community": "zu dieser Gemeinschaft wechseln" }, "decay": { "calculation_decay": "Berechnung der Vergänglichkeit", @@ -19,7 +27,6 @@ "decayStart": " - Startblock für Vergänglichkeit am: ", "decay_introduced": "Die Vergänglichkeit wurde Eingeführt am ", "decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion", - "fromCommunity": "Aus der Gemeinschaft", "hours": "Stunden", "last_transaction": "Letzte Transaktion", "minutes": "Minuten", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index d082a7133..4cb62d819 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -1,14 +1,22 @@ { "back": "Back", - "community": "Community", - "communitys": { - "form": { - "date_period": "Date / Period", - "hours": "hours", - "hours_report": "Hourly report", - "more_hours": "more hours", - "submit": "submit" - } + "community": { + "choose-another-community": "Choose another community", + "communities": { + "form": { + "date_period": "Date / Period", + "hours": "hours", + "hours_report": "Hourly report", + "more_hours": "more hours", + "submit": "submit" + } + }, + "community": "Community", + "continue-to-registration": "Continue to registration", + "current-community": "Current community", + "location": "Location:", + "other-communities": "Other communities", + "switch-to-this-community": "switch to this community" }, "decay": { "calculation_decay": "Calculation of Decay", @@ -19,7 +27,6 @@ "decayStart": " - Starting block for decay at: ", "decay_introduced": "Decay was Introduced on", "decay_since_last_transaction": "Decay since the last transaction", - "fromCommunity": "From the community", "hours": "Hours", "last_transaction": "Last transaction:", "minutes": "Minutes", diff --git a/frontend/src/main.js b/frontend/src/main.js index 5ca40591e..67863daff 100755 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -6,7 +6,11 @@ import { loadAllRules } from './validation-rules' import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost' import VueApollo from 'vue-apollo' import CONFIG from './config' + import VueApexCharts from 'vue-apexcharts' + +import addNavigationGuards from './routes/guards' + import { store } from './store/store' import router from './routes/router' @@ -49,13 +53,7 @@ Vue.config.productionTip = false loadAllRules(i18n) -router.beforeEach((to, from, next) => { - if (to.meta.requiresAuth && !store.state.token) { - next({ path: '/login' }) - } else { - next() - } -}) +addNavigationGuards(router, store) Vue.use(VueApexCharts) Vue.component('apexchart', VueApexCharts) diff --git a/frontend/src/plugins/globalDirectives.js b/frontend/src/plugins/globalDirectives.js index 56d8c9e13..1ef005f5b 100755 --- a/frontend/src/plugins/globalDirectives.js +++ b/frontend/src/plugins/globalDirectives.js @@ -1,4 +1,4 @@ -import clickOutside from '@/directives/click-ouside.js' +// import clickOutside from '@/directives/click-ouside.js' import { focus } from 'vue-focus' /** @@ -7,7 +7,7 @@ import { focus } from 'vue-focus' const GlobalDirectives = { install(Vue) { - Vue.directive('click-outside', clickOutside) + // Vue.directive('click-outside', clickOutside) Vue.directive('focus', focus) }, } diff --git a/frontend/src/routes/guards.js b/frontend/src/routes/guards.js new file mode 100644 index 000000000..eebd6976e --- /dev/null +++ b/frontend/src/routes/guards.js @@ -0,0 +1,18 @@ +const addNavigationGuards = (router, store) => { + router.beforeEach((to, from, next) => { + // handle publisherId + const publisherId = to.query.pid + if (publisherId) { + store.commit('publisherId', publisherId) + delete to.query.pid + } + // handle authentication + if (to.meta.requiresAuth && !store.state.token) { + next({ path: '/login' }) + } else { + next() + } + }) +} + +export default addNavigationGuards diff --git a/frontend/src/routes/guards.test.js b/frontend/src/routes/guards.test.js new file mode 100644 index 000000000..cf366eac8 --- /dev/null +++ b/frontend/src/routes/guards.test.js @@ -0,0 +1,47 @@ +import addNavigationGuards from './guards' +import router from './router' + +const storeCommitMock = jest.fn() + +const store = { + commit: storeCommitMock, + state: { + token: null, + }, +} + +addNavigationGuards(router, store) + +describe('navigation guards', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('publisher ID', () => { + it('commits the pid to the store when present', async () => { + await router.push({ path: 'register', query: { pid: 42 } }) + expect(storeCommitMock).toBeCalledWith('publisherId', '42') + }) + + it('does not commit the pid when not present', async () => { + await router.push({ path: 'password' }) + expect(storeCommitMock).not.toBeCalled() + }) + }) + + describe('authorization', () => { + const navGuard = router.beforeHooks[0] + const next = jest.fn() + + it('redirects to login when not authorized', () => { + navGuard({ meta: { requiresAuth: true }, query: {} }, {}, next) + expect(next).toBeCalledWith({ path: '/login' }) + }) + + it('does not redirect to login when authorized', () => { + store.state.token = 'valid token' + navGuard({ meta: { requiresAuth: true }, query: {} }, {}, next) + expect(next).toBeCalledWith() + }) + }) +}) diff --git a/frontend/src/routes/router.js b/frontend/src/routes/router.js index 189516242..f32c15fc1 100644 --- a/frontend/src/routes/router.js +++ b/frontend/src/routes/router.js @@ -5,10 +5,9 @@ import CONFIG from '../config' Vue.use(VueRouter) -// configure router const router = new VueRouter({ base: '/vue', - routes, // short for routes: routes + routes, linkActiveClass: 'active', mode: 'history', scrollBehavior: (to, from, savedPosition) => { diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index 57fca142b..5e2e16005 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -55,8 +55,8 @@ describe('router', () => { expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' }) }) - it('has ten routes defined', () => { - expect(routes).toHaveLength(10) + it('has twelve routes defined', () => { + expect(routes).toHaveLength(12) }) describe('overview', () => { @@ -131,6 +131,20 @@ describe('router', () => { }) }) + describe('register-community', () => { + it('loads the "registerCommunity" component', async () => { + const component = await routes.find((r) => r.path === '/register-community').component() + expect(component.default.name).toBe('registerCommunity') + }) + }) + + describe('select-community', () => { + it('loads the "registerSelectCommunity" component', async () => { + const component = await routes.find((r) => r.path === '/select-community').component() + expect(component.default.name).toBe('registerSelectCommunity') + }) + }) + describe('reset', () => { it('loads the "ResetPassword" component', async () => { const component = await routes.find((r) => r.path === '/reset/:optin').component() diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index a662de10d..1d6775e10 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -55,6 +55,14 @@ const routes = [ path: '/password', component: () => import('../views/Pages/ForgotPassword.vue'), }, + { + path: '/register-community', + component: () => import('../views/Pages/RegisterCommunity.vue'), + }, + { + path: '/select-community', + component: () => import('../views/Pages/RegisterSelectCommunity.vue'), + }, { path: '/reset/:optin', component: () => import('../views/Pages/ResetPassword.vue'), diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 5bb55c9d8..03e1915ca 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -29,6 +29,15 @@ export const mutations = { newsletterState: (state, newsletterState) => { state.newsletterState = newsletterState }, + publisherId: (state, publisherId) => { + state.publisherId = publisherId + }, + community: (state, community) => { + state.community = community + }, + coinanimation: (state, coinanimation) => { + state.coinanimation = coinanimation + }, } export const actions = { @@ -39,6 +48,7 @@ export const actions = { commit('firstName', data.firstName) commit('lastName', data.lastName) commit('description', data.description) + commit('coinanimation', data.coinanimation) commit('newsletterState', data.klickTipp.newsletterState) }, logout: ({ commit, state }) => { @@ -48,6 +58,7 @@ export const actions = { commit('firstName', '') commit('lastName', '') commit('description', '') + commit('coinanimation', true) commit('newsletterState', null) localStorage.clear() }, @@ -69,6 +80,7 @@ export const store = new Vuex.Store({ token: null, coinanimation: true, newsletterState: null, + community: null, }, getters: {}, // Syncronous mutation of the state diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 7ae3344d9..19937eed4 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -8,7 +8,10 @@ const { firstName, lastName, description, + coinanimation, newsletterState, + publisherId, + community, } = mutations const { login, logout } = actions @@ -70,6 +73,14 @@ describe('Vuex store', () => { }) }) + describe('coinanimation', () => { + it('sets the state of coinanimation', () => { + const state = { coinanimation: true } + coinanimation(state, false) + expect(state.coinanimation).toEqual(false) + }) + }) + describe('newsletterState', () => { it('sets the state of newsletterState', () => { const state = { newsletterState: null } @@ -77,6 +88,32 @@ describe('Vuex store', () => { expect(state.newsletterState).toEqual(true) }) }) + + describe('publisherId', () => { + it('sets the state of publisherId', () => { + const state = {} + publisherId(state, 42) + expect(state.publisherId).toEqual(42) + }) + }) + + describe('community', () => { + it('sets the state of community', () => { + const state = {} + community(state, { + name: 'test12', + description: 'test community 12', + url: 'http://test12.test12/', + registerUrl: 'http://test12.test12/vue/register', + }) + expect(state.community).toEqual({ + name: 'test12', + description: 'test community 12', + url: 'http://test12.test12/', + registerUrl: 'http://test12.test12/vue/register', + }) + }) + }) }) describe('actions', () => { @@ -90,14 +127,15 @@ describe('Vuex store', () => { firstName: 'Peter', lastName: 'Lustig', description: 'Nickelbrille', + coinanimation: false, klickTipp: { newsletterState: true, }, } - it('calls seven commits', () => { + it('calls eight commits', () => { login({ commit, state }, commitedData) - expect(commit).toHaveBeenCalledTimes(7) + expect(commit).toHaveBeenCalledTimes(8) }) it('commits email', () => { @@ -130,9 +168,14 @@ describe('Vuex store', () => { expect(commit).toHaveBeenNthCalledWith(6, 'description', 'Nickelbrille') }) + it('commits coinanimation', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(7, 'coinanimation', false) + }) + it('commits newsletterState', () => { login({ commit, state }, commitedData) - expect(commit).toHaveBeenNthCalledWith(7, 'newsletterState', true) + expect(commit).toHaveBeenNthCalledWith(8, 'newsletterState', true) }) }) @@ -140,9 +183,9 @@ describe('Vuex store', () => { const commit = jest.fn() const state = {} - it('calls six commits', () => { + it('calls eight commits', () => { logout({ commit, state }) - expect(commit).toHaveBeenCalledTimes(7) + expect(commit).toHaveBeenCalledTimes(8) }) it('commits token', () => { @@ -175,9 +218,14 @@ describe('Vuex store', () => { expect(commit).toHaveBeenNthCalledWith(6, 'description', '') }) + it('commits coinanimation', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(7, 'coinanimation', true) + }) + it('commits newsletterState', () => { logout({ commit, state }) - expect(commit).toHaveBeenNthCalledWith(7, 'newsletterState', null) + expect(commit).toHaveBeenNthCalledWith(8, 'newsletterState', null) }) // how to get this working? diff --git a/frontend/src/views/Pages/Login.spec.js b/frontend/src/views/Pages/Login.spec.js index 7218384f7..6aac3fda2 100644 --- a/frontend/src/views/Pages/Login.spec.js +++ b/frontend/src/views/Pages/Login.spec.js @@ -4,14 +4,20 @@ import Login from './Login' const localVue = global.localVue -const loginQueryMock = jest.fn().mockResolvedValue({ +const apolloQueryMock = jest.fn().mockResolvedValue({ data: { - login: 'token', + getCommunityInfo: { + name: 'test12', + description: 'test community 12', + url: 'http://test12.test12/', + registerUrl: 'http://test12.test12/vue/register', + }, }, }) const toastErrorMock = jest.fn() const mockStoreDispach = jest.fn() +const mockStoreCommit = jest.fn() const mockRouterPush = jest.fn() const spinnerHideMock = jest.fn() const spinnerMock = jest.fn(() => { @@ -30,6 +36,15 @@ describe('Login', () => { $t: jest.fn((t) => t), $store: { dispatch: mockStoreDispach, + commit: mockStoreCommit, + state: { + community: { + name: 'Gradido Entwicklung', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + }, + }, }, $loading: { show: spinnerMock, @@ -41,7 +56,7 @@ describe('Login', () => { error: toastErrorMock, }, $apollo: { - query: loginQueryMock, + query: apolloQueryMock, }, } @@ -62,20 +77,54 @@ describe('Login', () => { expect(wrapper.find('div.login-form').exists()).toBeTruthy() }) + it('commits the community info to the store', () => { + expect(mockStoreCommit).toBeCalledWith('community', { + name: 'test12', + description: 'test community 12', + url: 'http://test12.test12/', + registerUrl: 'http://test12.test12/vue/register', + }) + }) + + describe('communities gives back error', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ + message: 'Failed to get communities', + }) + wrapper = Wrapper() + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Failed to get communities') + }) + }) + describe('Login header', () => { it('has a welcome message', () => { expect(wrapper.find('div.header').text()).toBe('Gradido site.login.community') }) }) + describe('Community Data', () => { + it('has a Community name', () => { + expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung') + }) + + it('has a Community description', () => { + expect(wrapper.find('.test-communitydata p').text()).toBe( + 'Die lokale Entwicklungsumgebung von Gradido.', + ) + }) + }) + describe('links', () => { - it('has a link "Forgot Password?"', () => { + it('has a link "Forgot Password"', () => { expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual( 'settings.password.forgot_pwd', ) }) - it('links to /password when clicking "Forgot Password?"', () => { + it('links to /password when clicking "Forgot Password"', () => { expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/password') }) @@ -86,7 +135,9 @@ describe('Login', () => { }) it('links to /register when clicking "Create new account"', () => { - expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe('/register') + expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe( + '/register-community', + ) }) }) @@ -135,10 +186,15 @@ describe('Login', () => { await flushPromises() await wrapper.find('form').trigger('submit') await flushPromises() + apolloQueryMock.mockResolvedValue({ + data: { + login: 'token', + }, + }) }) it('calls the API with the given data', () => { - expect(loginQueryMock).toBeCalledWith( + expect(apolloQueryMock).toBeCalledWith( expect.objectContaining({ variables: { email: 'user@example.org', @@ -168,7 +224,7 @@ describe('Login', () => { describe('login fails', () => { beforeEach(() => { - loginQueryMock.mockRejectedValue({ + apolloQueryMock.mockRejectedValue({ message: 'Ouch!', }) }) diff --git a/frontend/src/views/Pages/Login.vue b/frontend/src/views/Pages/Login.vue index f5c6f025d..db8789002 100755 --- a/frontend/src/views/Pages/Login.vue +++ b/frontend/src/views/Pages/Login.vue @@ -18,9 +18,14 @@ -
- {{ $t('login') }} +
+ {{ $store.state.community.name }} +

+ {{ $store.state.community.description }} +

+ {{ $t('login') }}
+ @@ -38,13 +43,17 @@ - - + + {{ $t('settings.password.forgot_pwd') }} - - + + {{ $t('site.login.new_wallet') }} @@ -58,7 +67,7 @@ import CONFIG from '../../config' import InputPassword from '../../components/Inputs/InputPassword' import InputEmail from '../../components/Inputs/InputEmail' -import { login } from '../../graphql/queries' +import { login, communityInfo } from '../../graphql/queries' export default { name: 'login', @@ -103,6 +112,21 @@ export default { this.$toasted.error(this.$t('error.no-account')) }) }, + async onCreated() { + this.$apollo + .query({ + query: communityInfo, + }) + .then((result) => { + this.$store.commit('community', result.data.getCommunityInfo) + }) + .catch((error) => { + this.$toasted.error(error.message) + }) + }, + }, + created() { + this.onCreated() }, } diff --git a/frontend/src/views/Pages/Register.spec.js b/frontend/src/views/Pages/Register.spec.js index f3f66fca9..388a96746 100644 --- a/frontend/src/views/Pages/Register.spec.js +++ b/frontend/src/views/Pages/Register.spec.js @@ -26,6 +26,12 @@ describe('Register', () => { state: { email: 'peter@lustig.de', language: 'en', + community: { + name: 'Gradido Entwicklung', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + }, }, }, } @@ -53,6 +59,18 @@ describe('Register', () => { }) }) + describe('Community Data', () => { + it('has a Community name?', () => { + expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung') + }) + + it('has a Community description?', () => { + expect(wrapper.find('.test-communitydata p').text()).toBe( + 'Die lokale Entwicklungsumgebung von Gradido.', + ) + }) + }) + describe('links', () => { it('has a link "Back"', () => { expect(wrapper.find('.test-button-back').text()).toEqual('back') @@ -127,6 +145,18 @@ describe('Register', () => { }) }) + describe('link Choose another community', () => { + it('has a link "Choose another community"', () => { + expect(wrapper.find('.test-button-another-community').text()).toEqual( + 'community.choose-another-community', + ) + }) + + it('links to /select-community when clicking "Choose another community"', () => { + expect(wrapper.find('.test-button-another-community').props().to).toBe('/select-community') + }) + }) + describe('API calls', () => { beforeEach(() => { wrapper.find('#registerFirstname').setValue('Max') diff --git a/frontend/src/views/Pages/Register.vue b/frontend/src/views/Pages/Register.vue index f08ea286e..8e1a5d26d 100755 --- a/frontend/src/views/Pages/Register.vue +++ b/frontend/src/views/Pages/Register.vue @@ -13,15 +13,21 @@
+ + -
- {{ $t('signup') }} +
+ {{ $store.state.community.name }} +

+ {{ $store.state.community.description }} +

+
{{ $t('signup') }}
@@ -118,12 +124,13 @@
- - {{ $t('back') }} - - + + + {{ $t('back') }} + + @@ -138,6 +145,13 @@ +
+ + + {{ $t('community.choose-another-community') }} + + +
diff --git a/frontend/src/views/Pages/RegisterCommunity.spec.js b/frontend/src/views/Pages/RegisterCommunity.spec.js new file mode 100644 index 000000000..102db5891 --- /dev/null +++ b/frontend/src/views/Pages/RegisterCommunity.spec.js @@ -0,0 +1,82 @@ +import { mount, RouterLinkStub } from '@vue/test-utils' +import RegisterCommunity from './RegisterCommunity' + +const localVue = global.localVue + +describe('RegisterCommunity', () => { + let wrapper + + const mocks = { + $i18n: { + locale: 'en', + }, + $t: jest.fn((t) => t), + $store: { + state: { + community: { + name: 'Gradido Entwicklung', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + }, + }, + }, + } + + const stubs = { + RouterLink: RouterLinkStub, + } + + const Wrapper = () => { + return mount(RegisterCommunity, { localVue, mocks, stubs }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element "#register-community"', () => { + expect(wrapper.find('div#register-community').exists()).toBeTruthy() + }) + + describe('Displaying the current community info', () => { + it('has a current community name', () => { + expect(wrapper.find('.header h1').text()).toBe('Gradido Entwicklung') + }) + + it('has a current community description', () => { + expect(wrapper.find('.header p').text()).toBe( + 'Die lokale Entwicklungsumgebung von Gradido.', + ) + }) + + it('has a current community location', () => { + expect(wrapper.find('.header p.community-location').text()).toBe('http://localhost/vue/') + }) + }) + + describe('buttons and links', () => { + it('has a button "Continue to registration?"', () => { + expect(wrapper.findAll('a').at(0).text()).toEqual('community.continue-to-registration') + }) + it('button links to /register when clicking "Continue to registration"', () => { + expect(wrapper.findAll('a').at(0).props().to).toBe('/register') + }) + + it('has a button "Choose another community?"', () => { + expect(wrapper.findAll('a').at(1).text()).toEqual('community.choose-another-community') + }) + it('button links to /select-community when clicking "Choose another community"', () => { + expect(wrapper.findAll('a').at(1).props().to).toBe('/select-community') + }) + + it('has a button "Back to Login?"', () => { + expect(wrapper.findAll('a').at(2).text()).toEqual('back') + }) + it('button links to /login when clicking "Back to Login"', () => { + expect(wrapper.findAll('a').at(2).props().to).toBe('/login') + }) + }) + }) +}) diff --git a/frontend/src/views/Pages/RegisterCommunity.vue b/frontend/src/views/Pages/RegisterCommunity.vue new file mode 100644 index 000000000..9512edd1a --- /dev/null +++ b/frontend/src/views/Pages/RegisterCommunity.vue @@ -0,0 +1,60 @@ + + + diff --git a/frontend/src/views/Pages/RegisterSelectCommunity.spec.js b/frontend/src/views/Pages/RegisterSelectCommunity.spec.js new file mode 100644 index 000000000..dbcd950b8 --- /dev/null +++ b/frontend/src/views/Pages/RegisterSelectCommunity.spec.js @@ -0,0 +1,127 @@ +import { mount, RouterLinkStub } from '@vue/test-utils' +import RegisterSelectCommunity from './RegisterSelectCommunity' + +const localVue = global.localVue + +const spinnerHideMock = jest.fn() + +const spinnerMock = jest.fn(() => { + return { + hide: spinnerHideMock, + } +}) + +const apolloQueryMock = jest.fn().mockResolvedValue({ + data: { + communities: [ + { + id: 1, + name: 'Gradido Entwicklung', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register-community', + }, + { + id: 2, + name: 'Gradido Staging', + description: 'Der Testserver der Gradido-Akademie.', + url: 'https://stage1.gradido.net/vue/', + registerUrl: 'https://stage1.gradido.net/vue/register-community', + }, + { + id: 3, + name: 'Gradido-Akademie', + description: 'Freies Institut für Wirtschaftsbionik.', + url: 'https://gradido.net', + registerUrl: 'https://gdd1.gradido.com/vue/register-community', + }, + ], + }, +}) + +const toasterMock = jest.fn() + +describe('RegisterSelectCommunity', () => { + let wrapper + + const mocks = { + $i18n: { + locale: 'en', + }, + $t: jest.fn((t) => t), + $store: { + state: { + community: { + name: 'Gradido Entwicklung', + url: 'http://localhost/vue/', + registerUrl: 'http://localhost/vue/register', + description: 'Die lokale Entwicklungsumgebung von Gradido.', + }, + }, + }, + $apollo: { + query: apolloQueryMock, + }, + $loading: { + show: spinnerMock, + }, + $toasted: { + error: toasterMock, + }, + } + + const stubs = { + RouterLink: RouterLinkStub, + } + + const Wrapper = () => { + return mount(RegisterSelectCommunity, { localVue, mocks, stubs }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element "#register-select-community"', () => { + expect(wrapper.find('div#register-select-community').exists()).toBeTruthy() + }) + + it('starts with a spinner', () => { + expect(spinnerMock).toBeCalled() + }) + + describe('calls the apollo query', () => { + describe('server returns data', () => { + it('calls the API to get the data', () => { + expect(apolloQueryMock).toBeCalled() + }) + + it('shows two other communities', () => { + expect(wrapper.findAll('div.bg-secondary')).toHaveLength(2) + }) + + it('hides the spinner', () => { + expect(spinnerHideMock).toBeCalled() + }) + }) + + describe('server response is error', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ + message: 'Wrong thing', + }) + wrapper = Wrapper() + }) + + it('toast an error', () => { + expect(toasterMock).toBeCalledWith('Wrong thing') + }) + + it('hides the spinner', () => { + expect(spinnerHideMock).toBeCalled() + }) + }) + }) + }) +}) diff --git a/frontend/src/views/Pages/RegisterSelectCommunity.vue b/frontend/src/views/Pages/RegisterSelectCommunity.vue new file mode 100644 index 000000000..0d3c25220 --- /dev/null +++ b/frontend/src/views/Pages/RegisterSelectCommunity.vue @@ -0,0 +1,104 @@ + + + diff --git a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js index 6178805c8..463613449 100644 --- a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js +++ b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js @@ -67,12 +67,13 @@ describe('GddSend', () => { it('trims the email after blur', async () => { await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ') + await wrapper.find('#input-group-1').find('input').trigger('blur') await flushPromises() expect(wrapper.vm.form.email).toBe('valid@email.com') }) }) - describe('ammount field', () => { + describe('amount field', () => { it('has an input field of type text', () => { expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('text') }) @@ -91,6 +92,13 @@ describe('GddSend', () => { ) }) + it('does not update form amount when invalid', async () => { + await wrapper.find('#input-group-2').find('input').setValue('invalid') + await wrapper.find('#input-group-2').find('input').trigger('blur') + await flushPromises() + expect(wrapper.vm.form.amountValue).toBe(0) + }) + it('flushes an error message when no valid amount is given', async () => { await wrapper.find('#input-group-2').find('input').setValue('a') await flushPromises() @@ -150,11 +158,11 @@ describe('GddSend', () => { it('clears all fields on click', async () => { await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') await wrapper.find('#input-group-2').find('input').setValue('87.23') - await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh') + await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') await flushPromises() expect(wrapper.vm.form.email).toBe('someone@watches.tv') expect(wrapper.vm.form.amount).toBe('87.23') - expect(wrapper.vm.form.memo).toBe('Long enugh') + expect(wrapper.vm.form.memo).toBe('Long enough') await wrapper.find('button[type="reset"]').trigger('click') await flushPromises() expect(wrapper.vm.form.email).toBe('') @@ -167,7 +175,7 @@ describe('GddSend', () => { beforeEach(async () => { await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') await wrapper.find('#input-group-2').find('input').setValue('87.23') - await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh') + await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') await wrapper.find('form').trigger('submit') await flushPromises() }) @@ -179,7 +187,7 @@ describe('GddSend', () => { { email: 'someone@watches.tv', amount: 87.23, - memo: 'Long enugh', + memo: 'Long enough', }, ], ]) diff --git a/frontend/src/views/Pages/UserProfile/UserCard.spec.js b/frontend/src/views/Pages/UserProfile/UserCard.spec.js new file mode 100644 index 000000000..0fe13c416 --- /dev/null +++ b/frontend/src/views/Pages/UserProfile/UserCard.spec.js @@ -0,0 +1,36 @@ +import { mount } from '@vue/test-utils' +import UserCard from './UserCard' + +const localVue = global.localVue + +describe('UserCard', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $n: jest.fn((n) => String(n)), + $store: { + state: { + email: 'user@example.org', + }, + }, + } + + const Wrapper = () => { + return mount(UserCard, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".userdata-card"', () => { + expect(wrapper.find('div.userdata-card').exists()).toBeTruthy() + }) + + it('renders the Div Element "vue-qrcode"', () => { + expect(wrapper.find('vue-qrcode')) + }) + }) +}) diff --git a/frontend/src/views/Pages/UserProfile/UserCard.vue b/frontend/src/views/Pages/UserProfile/UserCard.vue index 1610eb7e9..4ddbb2726 100755 --- a/frontend/src/views/Pages/UserProfile/UserCard.vue +++ b/frontend/src/views/Pages/UserProfile/UserCard.vue @@ -1,30 +1,32 @@