feat(backend): badges (#8391)
* delete all old badges * reward/unrewardBadge * verification Badges * name all badged accordingly * more tests, lint * seed badges * profileBadge mechanic * badgesUnusedCount * seed profileBadges set * configure profile badge count * insert badges db:data:badges:default * seed commands to seed default badges and allow to seed branding data * copy data migrations when building docker * typo * correct data:branding command & document it * test new functionality * Update backend/src/db/seed/badges.ts Co-authored-by: Max <maxharz@gmail.com> * Update backend/src/db/seed/badges.ts Co-authored-by: Max <maxharz@gmail.com> * Update backend/src/db/seed/badges.ts Co-authored-by: Max <maxharz@gmail.com> * naming coventions * final naming fix lint fix build fix badge type in test renamed badge_ to trophy_ lint fixes small renameing fixes fix users spec fix webapp queries fix display * expose badge description --------- Co-authored-by: Max <maxharz@gmail.com>
@ -23,6 +23,7 @@ COPY . .
|
|||||||
ONBUILD COPY ./branding/constants/ src/config/tmp
|
ONBUILD COPY ./branding/constants/ src/config/tmp
|
||||||
ONBUILD RUN tools/replace-constants.sh
|
ONBUILD RUN tools/replace-constants.sh
|
||||||
ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
|
ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
|
||||||
|
ONBUILD COPY ./branding/data/ src/db/data
|
||||||
ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive
|
ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||||
ONBUILD RUN yarn run build
|
ONBUILD RUN yarn run build
|
||||||
ONBUILD RUN mkdir /build
|
ONBUILD RUN mkdir /build
|
||||||
|
|||||||
@ -120,6 +120,20 @@ When using `CATEGORIES_ACTIVE=true` you also want to seed the categories with:
|
|||||||
yarn db:data:categories
|
yarn db:data:categories
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Branding Data
|
||||||
|
|
||||||
|
You might need to seed some branding specific data into the database.
|
||||||
|
|
||||||
|
To do so, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# in backend with database running (In docker or local)
|
||||||
|
yarn db:data:branding
|
||||||
|
|
||||||
|
# for docker
|
||||||
|
docker exec backend yarn db:data:branding
|
||||||
|
```
|
||||||
|
|
||||||
### Seed Data
|
### Seed Data
|
||||||
|
|
||||||
For a predefined set of test data you can seed the database with:
|
For a predefined set of test data you can seed the database with:
|
||||||
|
|||||||
0
backend/branding/data/.gitkeep
Normal file
@ -18,6 +18,8 @@
|
|||||||
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
|
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
|
||||||
"db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts",
|
"db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts",
|
||||||
"db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts",
|
"db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts",
|
||||||
|
"db:data:badges": "ts-node --require tsconfig-paths/register src/db/badges.ts",
|
||||||
|
"db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-production.ts",
|
||||||
"db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts",
|
"db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts",
|
||||||
"db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts",
|
"db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts",
|
||||||
"db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create",
|
"db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 637 B After Width: | Height: | Size: 637 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
2
backend/src/constants/badges.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js`
|
||||||
|
export const TROPHY_BADGES_SELECTED_MAX = 9
|
||||||
13
backend/src/db/badges.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { getNeode } from './neo4j'
|
||||||
|
import { trophies, verification } from './seed/badges'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/newline-after-import
|
||||||
|
;(async function () {
|
||||||
|
const neode = getNeode()
|
||||||
|
try {
|
||||||
|
await trophies()
|
||||||
|
await verification()
|
||||||
|
} finally {
|
||||||
|
await neode.close()
|
||||||
|
}
|
||||||
|
})()
|
||||||
17
backend/src/db/data-branding.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { readdir } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const dataFolder = path.join(__dirname, 'data/')
|
||||||
|
|
||||||
|
;(async function () {
|
||||||
|
const files = await readdir(dataFolder)
|
||||||
|
files.forEach(async (file) => {
|
||||||
|
if (file.slice(0, -3).endsWith('-branding')) {
|
||||||
|
const importedModule = await import(path.join(dataFolder, file))
|
||||||
|
if (!importedModule.default) {
|
||||||
|
throw new Error('Your data file must export a default function')
|
||||||
|
}
|
||||||
|
await importedModule.default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
0
backend/src/db/data/.gitkeep
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { getDriver } from '@db/neo4j'
|
||||||
|
|
||||||
|
export const description = ''
|
||||||
|
|
||||||
|
export async function up(next) {
|
||||||
|
const driver = getDriver()
|
||||||
|
const session = driver.session()
|
||||||
|
const transaction = session.beginTransaction()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Implement your migration here.
|
||||||
|
await transaction.run(`
|
||||||
|
MATCH (badge:Badge)
|
||||||
|
DETACH DELETE badge
|
||||||
|
`)
|
||||||
|
await transaction.commit()
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error)
|
||||||
|
await transaction.rollback()
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('rolled back')
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(next) {
|
||||||
|
const driver = getDriver()
|
||||||
|
const session = driver.session()
|
||||||
|
const transaction = session.beginTransaction()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// cannot be rolled back
|
||||||
|
// Implement your migration here.
|
||||||
|
// await transaction.run(``)
|
||||||
|
// await transaction.commit()
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error)
|
||||||
|
await transaction.rollback()
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('rolled back')
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import createServer from '@src/server'
|
|||||||
|
|
||||||
import Factory from './factories'
|
import Factory from './factories'
|
||||||
import { getNeode, getDriver } from './neo4j'
|
import { getNeode, getDriver } from './neo4j'
|
||||||
|
import { trophies, verification } from './seed/badges'
|
||||||
|
|
||||||
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
|
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
|
||||||
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
|
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
|
||||||
@ -124,32 +125,28 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
await Hamburg.relateTo(Germany, 'isIn')
|
await Hamburg.relateTo(Germany, 'isIn')
|
||||||
await Paris.relateTo(France, 'isIn')
|
await Paris.relateTo(France, 'isIn')
|
||||||
|
|
||||||
// badges
|
const {
|
||||||
const racoon = await Factory.build('badge', {
|
trophyAirship,
|
||||||
id: 'indiegogo_en_racoon',
|
trophyBee,
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
trophyStarter,
|
||||||
})
|
trophyFlower,
|
||||||
const rabbit = await Factory.build('badge', {
|
trophyPanda,
|
||||||
id: 'indiegogo_en_rabbit',
|
trophyTiger,
|
||||||
icon: '/img/badges/indiegogo_en_rabbit.svg',
|
trophyAlienship,
|
||||||
})
|
trophyBalloon,
|
||||||
const wolf = await Factory.build('badge', {
|
trophyMagicrainbow,
|
||||||
id: 'indiegogo_en_wolf',
|
trophySuperfounder,
|
||||||
icon: '/img/badges/indiegogo_en_wolf.svg',
|
trophyBigballoon,
|
||||||
})
|
trophyLifetree,
|
||||||
const bear = await Factory.build('badge', {
|
trophyRacoon,
|
||||||
id: 'indiegogo_en_bear',
|
trophyRhino,
|
||||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
trophyWolf,
|
||||||
})
|
trophyTurtle,
|
||||||
const turtle = await Factory.build('badge', {
|
trophyBear,
|
||||||
id: 'indiegogo_en_turtle',
|
trophyRabbit,
|
||||||
icon: '/img/badges/indiegogo_en_turtle.svg',
|
} = await trophies()
|
||||||
})
|
|
||||||
const rhino = await Factory.build('badge', {
|
|
||||||
id: 'indiegogo_en_rhino',
|
|
||||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const { verificationAdmin, verificationModerator, verificationDeveloper } = await verification()
|
||||||
// users
|
// users
|
||||||
const peterLustig = await Factory.build(
|
const peterLustig = await Factory.build(
|
||||||
'user',
|
'user',
|
||||||
@ -243,14 +240,50 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
await jennyRostock.relateTo(Paris, 'isIn')
|
await jennyRostock.relateTo(Paris, 'isIn')
|
||||||
await huey.relateTo(Paris, 'isIn')
|
await huey.relateTo(Paris, 'isIn')
|
||||||
|
|
||||||
await peterLustig.relateTo(racoon, 'rewarded')
|
// badges
|
||||||
await peterLustig.relateTo(rhino, 'rewarded')
|
await peterLustig.relateTo(trophyRacoon, 'rewarded')
|
||||||
await peterLustig.relateTo(wolf, 'rewarded')
|
await peterLustig.relateTo(trophyRhino, 'rewarded')
|
||||||
await bobDerBaumeister.relateTo(racoon, 'rewarded')
|
await peterLustig.relateTo(trophyWolf, 'rewarded')
|
||||||
await bobDerBaumeister.relateTo(turtle, 'rewarded')
|
await peterLustig.relateTo(trophyAirship, 'rewarded')
|
||||||
await jennyRostock.relateTo(bear, 'rewarded')
|
await peterLustig.relateTo(verificationAdmin, 'verifies')
|
||||||
await dagobert.relateTo(rabbit, 'rewarded')
|
await peterLustig.relateTo(trophyRacoon, 'selected', { slot: 0 })
|
||||||
|
await peterLustig.relateTo(trophyRhino, 'selected', { slot: 1 })
|
||||||
|
await peterLustig.relateTo(trophyAirship, 'selected', { slot: 5 })
|
||||||
|
|
||||||
|
await bobDerBaumeister.relateTo(trophyRacoon, 'rewarded')
|
||||||
|
await bobDerBaumeister.relateTo(trophyTurtle, 'rewarded')
|
||||||
|
await bobDerBaumeister.relateTo(trophyBee, 'rewarded')
|
||||||
|
await bobDerBaumeister.relateTo(verificationModerator, 'verifies')
|
||||||
|
await bobDerBaumeister.relateTo(trophyRacoon, 'selected', { slot: 1 })
|
||||||
|
await bobDerBaumeister.relateTo(trophyTurtle, 'selected', { slot: 2 })
|
||||||
|
|
||||||
|
await jennyRostock.relateTo(trophyBear, 'rewarded')
|
||||||
|
await jennyRostock.relateTo(trophyStarter, 'rewarded')
|
||||||
|
await jennyRostock.relateTo(trophyFlower, 'rewarded')
|
||||||
|
await jennyRostock.relateTo(trophyBear, 'selected', { slot: 0 })
|
||||||
|
await jennyRostock.relateTo(trophyStarter, 'selected', { slot: 1 })
|
||||||
|
await jennyRostock.relateTo(trophyFlower, 'selected', { slot: 2 })
|
||||||
|
|
||||||
|
await huey.relateTo(trophyPanda, 'rewarded')
|
||||||
|
await huey.relateTo(trophyTiger, 'rewarded')
|
||||||
|
await huey.relateTo(trophyAlienship, 'rewarded')
|
||||||
|
await huey.relateTo(trophyBalloon, 'rewarded')
|
||||||
|
await huey.relateTo(trophyMagicrainbow, 'rewarded')
|
||||||
|
await huey.relateTo(trophySuperfounder, 'rewarded')
|
||||||
|
await huey.relateTo(verificationDeveloper, 'verifies')
|
||||||
|
await huey.relateTo(trophyPanda, 'selected', { slot: 0 })
|
||||||
|
await huey.relateTo(trophyTiger, 'selected', { slot: 1 })
|
||||||
|
await huey.relateTo(trophyAlienship, 'selected', { slot: 2 })
|
||||||
|
|
||||||
|
await dewey.relateTo(trophyBigballoon, 'rewarded')
|
||||||
|
await dewey.relateTo(trophyLifetree, 'rewarded')
|
||||||
|
await dewey.relateTo(trophyBigballoon, 'selected', { slot: 7 })
|
||||||
|
await dewey.relateTo(trophyLifetree, 'selected', { slot: 8 })
|
||||||
|
|
||||||
|
await louie.relateTo(trophyRabbit, 'rewarded')
|
||||||
|
await louie.relateTo(trophyRabbit, 'selected', { slot: 4 })
|
||||||
|
|
||||||
|
// Friends
|
||||||
await peterLustig.relateTo(bobDerBaumeister, 'friends')
|
await peterLustig.relateTo(bobDerBaumeister, 'friends')
|
||||||
await peterLustig.relateTo(jennyRostock, 'friends')
|
await peterLustig.relateTo(jennyRostock, 'friends')
|
||||||
await bobDerBaumeister.relateTo(jennyRostock, 'friends')
|
await bobDerBaumeister.relateTo(jennyRostock, 'friends')
|
||||||
|
|||||||
185
backend/src/db/seed/badges.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import Factory from '@db/factories'
|
||||||
|
|
||||||
|
export const trophies = async () => {
|
||||||
|
return {
|
||||||
|
// Blue Animals
|
||||||
|
trophyBear: await Factory.build('badge', {
|
||||||
|
id: 'trophy_bear',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Bear',
|
||||||
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
|
}),
|
||||||
|
trophyPanda: await Factory.build('badge', {
|
||||||
|
id: 'trophy_panda',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Panda',
|
||||||
|
icon: '/img/badges/trophy_blue_panda.svg',
|
||||||
|
}),
|
||||||
|
trophyRabbit: await Factory.build('badge', {
|
||||||
|
id: 'trophy_rabbit',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Rabbit',
|
||||||
|
icon: '/img/badges/trophy_blue_rabbit.svg',
|
||||||
|
}),
|
||||||
|
trophyRacoon: await Factory.build('badge', {
|
||||||
|
id: 'trophy_racoon',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Racoon',
|
||||||
|
icon: '/img/badges/trophy_blue_racoon.svg',
|
||||||
|
}),
|
||||||
|
trophyRhino: await Factory.build('badge', {
|
||||||
|
id: 'trophy_rhino',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Rhino',
|
||||||
|
icon: '/img/badges/trophy_blue_rhino.svg',
|
||||||
|
}),
|
||||||
|
trophyTiger: await Factory.build('badge', {
|
||||||
|
id: 'trophy_tiger',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Tiger',
|
||||||
|
icon: '/img/badges/trophy_blue_tiger.svg',
|
||||||
|
}),
|
||||||
|
trophyTurtle: await Factory.build('badge', {
|
||||||
|
id: 'trophy_turtle',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Turtle',
|
||||||
|
icon: '/img/badges/trophy_blue_turtle.svg',
|
||||||
|
}),
|
||||||
|
trophyWhale: await Factory.build('badge', {
|
||||||
|
id: 'trophy_whale',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Whale',
|
||||||
|
icon: '/img/badges/trophy_blue_whale.svg',
|
||||||
|
}),
|
||||||
|
trophyWolf: await Factory.build('badge', {
|
||||||
|
id: 'trophy_wolf',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Wolf',
|
||||||
|
icon: '/img/badges/trophy_blue_wolf.svg',
|
||||||
|
}),
|
||||||
|
// Green Transports
|
||||||
|
trophyAirship: await Factory.build('badge', {
|
||||||
|
id: 'trophy_airship',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned an Airship',
|
||||||
|
icon: '/img/badges/trophy_green_airship.svg',
|
||||||
|
}),
|
||||||
|
trophyAlienship: await Factory.build('badge', {
|
||||||
|
id: 'trophy_alienship',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned an Alienship',
|
||||||
|
icon: '/img/badges/trophy_green_alienship.svg',
|
||||||
|
}),
|
||||||
|
trophyBalloon: await Factory.build('badge', {
|
||||||
|
id: 'trophy_balloon',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Balloon',
|
||||||
|
icon: '/img/badges/trophy_green_balloon.svg',
|
||||||
|
}),
|
||||||
|
trophyBigballoon: await Factory.build('badge', {
|
||||||
|
id: 'trophy_bigballoon',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Big Balloon',
|
||||||
|
icon: '/img/badges/trophy_green_bigballoon.svg',
|
||||||
|
}),
|
||||||
|
trophyCrane: await Factory.build('badge', {
|
||||||
|
id: 'trophy_crane',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Crane',
|
||||||
|
icon: '/img/badges/trophy_green_crane.svg',
|
||||||
|
}),
|
||||||
|
trophyGlider: await Factory.build('badge', {
|
||||||
|
id: 'trophy_glider',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Glider',
|
||||||
|
icon: '/img/badges/trophy_green_glider.svg',
|
||||||
|
}),
|
||||||
|
trophyHelicopter: await Factory.build('badge', {
|
||||||
|
id: 'trophy_helicopter',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Helicopter',
|
||||||
|
icon: '/img/badges/trophy_green_helicopter.svg',
|
||||||
|
}),
|
||||||
|
// Green Animals
|
||||||
|
trophyBee: await Factory.build('badge', {
|
||||||
|
id: 'trophy_bee',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Bee',
|
||||||
|
icon: '/img/badges/trophy_green_bee.svg',
|
||||||
|
}),
|
||||||
|
trophyButterfly: await Factory.build('badge', {
|
||||||
|
id: 'trophy_butterfly',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Butterfly',
|
||||||
|
icon: '/img/badges/trophy_green_butterfly.svg',
|
||||||
|
}),
|
||||||
|
// Green Plants
|
||||||
|
trophyFlower: await Factory.build('badge', {
|
||||||
|
id: 'trophy_flower',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Flower',
|
||||||
|
icon: '/img/badges/trophy_green_flower.svg',
|
||||||
|
}),
|
||||||
|
trophyLifetree: await Factory.build('badge', {
|
||||||
|
id: 'trophy_lifetree',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the tree of life',
|
||||||
|
icon: '/img/badges/trophy_green_lifetree.svg',
|
||||||
|
}),
|
||||||
|
// Green Misc
|
||||||
|
trophyDoublerainbow: await Factory.build('badge', {
|
||||||
|
id: 'trophy_doublerainbow',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the Double Rainbow',
|
||||||
|
icon: '/img/badges/trophy_green_doublerainbow.svg',
|
||||||
|
}),
|
||||||
|
trophyEndrainbow: await Factory.build('badge', {
|
||||||
|
id: 'trophy_endrainbow',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the End of the Rainbow',
|
||||||
|
icon: '/img/badges/trophy_green_endrainbow.svg',
|
||||||
|
}),
|
||||||
|
trophyMagicrainbow: await Factory.build('badge', {
|
||||||
|
id: 'trophy_magicrainbow',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the Magic Rainbow',
|
||||||
|
icon: '/img/badges/trophy_green_magicrainbow.svg',
|
||||||
|
}),
|
||||||
|
trophyStarter: await Factory.build('badge', {
|
||||||
|
id: 'trophy_starter',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the Starter Badge',
|
||||||
|
icon: '/img/badges/trophy_green_starter.svg',
|
||||||
|
}),
|
||||||
|
trophySuperfounder: await Factory.build('badge', {
|
||||||
|
id: 'trophy_superfounder',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned the Super Founder Badge',
|
||||||
|
icon: '/img/badges/trophy_green_superfounder.svg',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verification = async () => {
|
||||||
|
return {
|
||||||
|
// Red Role
|
||||||
|
verificationModerator: await Factory.build('badge', {
|
||||||
|
id: 'verification_moderator',
|
||||||
|
type: 'verification',
|
||||||
|
description: 'You are a Moderator',
|
||||||
|
icon: '/img/badges/verification_red_moderator.svg',
|
||||||
|
}),
|
||||||
|
verificationAdmin: await Factory.build('badge', {
|
||||||
|
id: 'verification_admin',
|
||||||
|
type: 'verification',
|
||||||
|
description: 'You are an Administrator',
|
||||||
|
icon: '/img/badges/verification_red_admin.svg',
|
||||||
|
}),
|
||||||
|
verificationDeveloper: await Factory.build('badge', {
|
||||||
|
id: 'verification_developer',
|
||||||
|
type: 'verification',
|
||||||
|
description: 'You are a Developer',
|
||||||
|
icon: '/img/badges/verification_red_developer.svg',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -429,10 +429,9 @@ export default shield(
|
|||||||
CreateSocialMedia: isAuthenticated,
|
CreateSocialMedia: isAuthenticated,
|
||||||
UpdateSocialMedia: isMySocialMedia,
|
UpdateSocialMedia: isMySocialMedia,
|
||||||
DeleteSocialMedia: isMySocialMedia,
|
DeleteSocialMedia: isMySocialMedia,
|
||||||
// AddBadgeRewarded: isAdmin,
|
setVerificationBadge: isAdmin,
|
||||||
// RemoveBadgeRewarded: isAdmin,
|
rewardTrophyBadge: isAdmin,
|
||||||
reward: isAdmin,
|
revokeBadge: isAdmin,
|
||||||
unreward: isAdmin,
|
|
||||||
followUser: isAuthenticated,
|
followUser: isAuthenticated,
|
||||||
unfollowUser: isAuthenticated,
|
unfollowUser: isAuthenticated,
|
||||||
shout: isAuthenticated,
|
shout: isAuthenticated,
|
||||||
@ -469,6 +468,8 @@ export default shield(
|
|||||||
toggleObservePost: isAuthenticated,
|
toggleObservePost: isAuthenticated,
|
||||||
muteGroup: and(isAuthenticated, isMemberOfGroup),
|
muteGroup: and(isAuthenticated, isMemberOfGroup),
|
||||||
unmuteGroup: and(isAuthenticated, isMemberOfGroup),
|
unmuteGroup: and(isAuthenticated, isMemberOfGroup),
|
||||||
|
setTrophyBadgeSelected: isAuthenticated,
|
||||||
|
resetTrophyBadgesSelected: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
id: { type: 'string', primary: true, lowercase: true },
|
id: { type: 'string', primary: true, lowercase: true },
|
||||||
status: { type: 'string', valid: ['permanent', 'temporary'] },
|
type: { type: 'string', valid: ['verification', 'trophy'] },
|
||||||
type: { type: 'string', valid: ['role', 'crowdfunding'] },
|
|
||||||
icon: { type: 'string', required: true },
|
icon: { type: 'string', required: true },
|
||||||
|
description: { type: 'string', required: true },
|
||||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,24 @@ export default {
|
|||||||
target: 'Badge',
|
target: 'Badge',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
|
selected: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'SELECTED',
|
||||||
|
target: 'Badge',
|
||||||
|
direction: 'out',
|
||||||
|
properties: {
|
||||||
|
slot: {
|
||||||
|
type: 'int',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verifies: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'VERIFIES',
|
||||||
|
target: 'Badge',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
||||||
lastActiveAt: { type: 'string', isoDate: true },
|
lastActiveAt: { type: 'string', isoDate: true },
|
||||||
lastOnlineStatus: { type: 'string' },
|
lastOnlineStatus: { type: 'string' },
|
||||||
|
|||||||
667
backend/src/schema/resolvers/badges.spec.ts
Normal file
@ -0,0 +1,667 @@
|
|||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
|
import { getNeode, getDriver } from '@db/neo4j'
|
||||||
|
import createServer from '@src/server'
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
const instance = getNeode()
|
||||||
|
|
||||||
|
let authenticatedUser, regularUser, administrator, moderator, badge, verification, query, mutate
|
||||||
|
|
||||||
|
describe('Badges', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode: instance,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
driver.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
regularUser = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'regular-user-id',
|
||||||
|
role: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'user@example.org',
|
||||||
|
password: '1234',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
moderator = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'moderator-id',
|
||||||
|
role: 'moderator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'moderator@example.org',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
administrator = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'admin-id',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'admin@example.org',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
badge = await Factory.build('badge', {
|
||||||
|
id: 'trophy_rhino',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a rhino',
|
||||||
|
icon: '/img/badges/trophy_blue_rhino.svg',
|
||||||
|
})
|
||||||
|
|
||||||
|
verification = await Factory.build('badge', {
|
||||||
|
id: 'verification_moderator',
|
||||||
|
type: 'verification',
|
||||||
|
description: 'You are a moderator',
|
||||||
|
icon: '/img/badges/verification_red_moderator.svg',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||||
|
afterEach(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setVerificationBadge', () => {
|
||||||
|
const variables = {
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVerificationBadgeMutation = gql`
|
||||||
|
mutation ($badgeId: ID!, $userId: ID!) {
|
||||||
|
setVerificationBadge(badgeId: $badgeId, userId: $userId) {
|
||||||
|
id
|
||||||
|
badgeVerification {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophies {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
authenticatedUser = null
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: setVerificationBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { setVerificationBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as moderator', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = moderator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rewards badge to user', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: setVerificationBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { setVerificationBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as admin', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await administrator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('badge for id does not exist', () => {
|
||||||
|
it('rejects with an informative error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { setVerificationBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('non-existent user', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: { userId: 'non-existent-user-id', badgeId: 'verification_moderator' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { setVerificationBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('badge is not a verification badge', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: { userId: 'regular-user-id', badgeId: 'trophy_rhino' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { setVerificationBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rewards a verification badge to the user', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
setVerificationBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: { id: 'verification_moderator' },
|
||||||
|
badgeTrophies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: setVerificationBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overrides the existing verification if a second verification badge is rewarded to the same user', async () => {
|
||||||
|
await Factory.build('badge', {
|
||||||
|
id: 'verification_admin',
|
||||||
|
type: 'verification',
|
||||||
|
description: 'You are an admin',
|
||||||
|
icon: '/img/badges/verification_red_admin.svg',
|
||||||
|
})
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
setVerificationBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: { id: 'verification_admin' },
|
||||||
|
badgeTrophies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
badgeId: 'verification_admin',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rewards the same verification badge as well to another user', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
setVerificationBadge: {
|
||||||
|
id: 'regular-user-2-id',
|
||||||
|
badgeVerification: { id: 'verification_moderator' },
|
||||||
|
badgeTrophies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'regular-user-2-id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'regular2@email.com',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setVerificationBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-2-id',
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rewardTrophyBadge', () => {
|
||||||
|
const variables = {
|
||||||
|
badgeId: 'trophy_rhino',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewardTrophyBadgeMutation = gql`
|
||||||
|
mutation ($badgeId: ID!, $userId: ID!) {
|
||||||
|
rewardTrophyBadge(badgeId: $badgeId, userId: $userId) {
|
||||||
|
id
|
||||||
|
badgeVerification {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophies {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
authenticatedUser = null
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: rewardTrophyBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { rewardTrophyBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as moderator', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = moderator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rewards badge to user', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: rewardTrophyBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { rewardTrophyBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as admin', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await administrator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('badge for id does not exist', () => {
|
||||||
|
it('rejects with an informative error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { rewardTrophyBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('non-existent user', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: { userId: 'non-existent-user-id', badgeId: 'trophy_rhino' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { rewardTrophyBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('badge is a verification Badge', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: { userId: 'regular-user-id', badgeId: 'verification_moderator' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { rewardTrophyBadge: null },
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rewards a badge to the user', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
rewardTrophyBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: null,
|
||||||
|
badgeTrophies: [{ id: 'trophy_rhino' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: rewardTrophyBadgeMutation, variables }),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rewards a second different badge to the same user', async () => {
|
||||||
|
await Factory.build('badge', {
|
||||||
|
id: 'trophy_racoon',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a racoon',
|
||||||
|
icon: '/img/badges/trophy_blue_racoon.svg',
|
||||||
|
})
|
||||||
|
const trophies = [{ id: 'trophy_racoon' }, { id: 'trophy_rhino' }]
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
rewardTrophyBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeTrophies: expect.arrayContaining(trophies),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
badgeId: 'trophy_rhino',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
badgeId: 'trophy_racoon',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rewards the same badge as well to another user', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
rewardTrophyBadge: {
|
||||||
|
id: 'regular-user-2-id',
|
||||||
|
badgeTrophies: [{ id: 'trophy_rhino' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'regular-user-2-id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'regular2@email.com',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
userId: 'regular-user-2-id',
|
||||||
|
badgeId: 'trophy_rhino',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates no duplicate reward relationships', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: rewardTrophyBadgeMutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
|
||||||
|
const userQuery = gql`
|
||||||
|
{
|
||||||
|
User(id: "regular-user-id") {
|
||||||
|
badgeTrophiesCount
|
||||||
|
badgeTrophies {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const expected = {
|
||||||
|
data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] },
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(query({ query: userQuery })).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('revokeBadge', () => {
|
||||||
|
const variables = {
|
||||||
|
badgeId: 'trophy_rhino',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await regularUser.relateTo(badge, 'rewarded')
|
||||||
|
await regularUser.relateTo(verification, 'verifies')
|
||||||
|
})
|
||||||
|
|
||||||
|
const revokeBadgeMutation = gql`
|
||||||
|
mutation ($badgeId: ID!, $userId: ID!) {
|
||||||
|
revokeBadge(badgeId: $badgeId, userId: $userId) {
|
||||||
|
id
|
||||||
|
badgeVerification {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophies {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('check test setup', () => {
|
||||||
|
it('user has one badge', async () => {
|
||||||
|
authenticatedUser = regularUser.toJson()
|
||||||
|
const userQuery = gql`
|
||||||
|
{
|
||||||
|
User(id: "regular-user-id") {
|
||||||
|
badgeTrophiesCount
|
||||||
|
badgeTrophies {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const expected = {
|
||||||
|
data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] },
|
||||||
|
errors: undefined,
|
||||||
|
}
|
||||||
|
await expect(query({ query: userQuery })).resolves.toMatchObject(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
authenticatedUser = null
|
||||||
|
await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({
|
||||||
|
data: { revokeBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated moderator', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await moderator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('removes badge from user', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject(
|
||||||
|
{
|
||||||
|
data: { revokeBadge: null },
|
||||||
|
errors: [{ message: 'Not Authorized!' }],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated admin', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await administrator.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes a badge from user', async () => {
|
||||||
|
await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
revokeBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: { id: 'verification_moderator' },
|
||||||
|
badgeTrophies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not crash when revoking multiple times', async () => {
|
||||||
|
await mutate({ mutation: revokeBadgeMutation, variables })
|
||||||
|
await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
revokeBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: { id: 'verification_moderator' },
|
||||||
|
badgeTrophies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes a verification from user', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: revokeBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
revokeBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: null,
|
||||||
|
badgeTrophies: [{ id: 'trophy_rhino' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not crash when removing verification multiple times', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: revokeBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: revokeBadgeMutation,
|
||||||
|
variables: {
|
||||||
|
badgeId: 'verification_moderator',
|
||||||
|
userId: 'regular-user-id',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
revokeBadge: {
|
||||||
|
id: 'regular-user-id',
|
||||||
|
badgeVerification: null,
|
||||||
|
badgeTrophies: [{ id: 'trophy_rhino' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -2,8 +2,119 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
Badge: async (object, args, context, resolveInfo) => {
|
Badge: async (object, args, context, resolveInfo) =>
|
||||||
return neo4jgraphql(object, args, context, resolveInfo)
|
neo4jgraphql(object, args, context, resolveInfo),
|
||||||
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
setVerificationBadge: async (_object, args, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: currentUserId },
|
||||||
|
} = context
|
||||||
|
const { badgeId, userId } = args
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const response = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (badge:Badge {id: $badgeId, type: 'verification'}), (user:User {id: $userId})
|
||||||
|
OPTIONAL MATCH (:Badge {type: 'verification'})-[verify:VERIFIES]->(user)
|
||||||
|
DELETE verify
|
||||||
|
MERGE (badge)-[relation:VERIFIES {by: $currentUserId}]->(user)
|
||||||
|
RETURN relation, user {.*}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
badgeId,
|
||||||
|
userId,
|
||||||
|
currentUserId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
relation: response.records.map((record) => record.get('relation'))[0],
|
||||||
|
user: response.records.map((record) => record.get('user'))[0],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const { relation, user } = await writeTxResultPromise
|
||||||
|
if (!relation) {
|
||||||
|
throw new Error(
|
||||||
|
'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rewardTrophyBadge: async (_object, args, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: currentUserId },
|
||||||
|
} = context
|
||||||
|
const { badgeId, userId } = args
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const response = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (badge:Badge {id: $badgeId, type: 'trophy'}), (user:User {id: $userId})
|
||||||
|
MERGE (badge)-[relation:REWARDED {by: $currentUserId}]->(user)
|
||||||
|
RETURN relation, user {.*}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
badgeId,
|
||||||
|
userId,
|
||||||
|
currentUserId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
relation: response.records.map((record) => record.get('relation'))[0],
|
||||||
|
user: response.records.map((record) => record.get('user'))[0],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const { relation, user } = await writeTxResultPromise
|
||||||
|
if (!relation) {
|
||||||
|
throw new Error(
|
||||||
|
'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
revokeBadge: async (_object, args, context, _resolveInfo) => {
|
||||||
|
const { badgeId, userId } = args
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const response = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})
|
||||||
|
OPTIONAL MATCH (badge:Badge {id: $badgeId})-[relation:REWARDED|VERIFIES]->(user)
|
||||||
|
DELETE relation
|
||||||
|
RETURN user {.*}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
badgeId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response.records.map((record) => record.get('user'))[0]
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
return await writeTxResultPromise
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,352 +0,0 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
|
||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
|
||||||
import { getNeode, getDriver } from '@db/neo4j'
|
|
||||||
import createServer from '@src/server'
|
|
||||||
|
|
||||||
const driver = getDriver()
|
|
||||||
const instance = getNeode()
|
|
||||||
|
|
||||||
let authenticatedUser, regularUser, administrator, moderator, badge, query, mutate
|
|
||||||
|
|
||||||
describe('rewards', () => {
|
|
||||||
const variables = {
|
|
||||||
from: 'indiegogo_en_rhino',
|
|
||||||
to: 'regular-user-id',
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
|
|
||||||
const { server } = createServer({
|
|
||||||
context: () => {
|
|
||||||
return {
|
|
||||||
driver,
|
|
||||||
neode: instance,
|
|
||||||
user: authenticatedUser,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
query = createTestClient(server).query
|
|
||||||
mutate = createTestClient(server).mutate
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
driver.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
regularUser = await Factory.build(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
id: 'regular-user-id',
|
|
||||||
role: 'user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'user@example.org',
|
|
||||||
password: '1234',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
moderator = await Factory.build(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
id: 'moderator-id',
|
|
||||||
role: 'moderator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'moderator@example.org',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
administrator = await Factory.build(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
id: 'admin-id',
|
|
||||||
role: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'admin@example.org',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
badge = await Factory.build('badge', {
|
|
||||||
id: 'indiegogo_en_rhino',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('reward', () => {
|
|
||||||
const rewardMutation = gql`
|
|
||||||
mutation ($from: ID!, $to: ID!) {
|
|
||||||
reward(badgeKey: $from, userId: $to) {
|
|
||||||
id
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
authenticatedUser = null
|
|
||||||
await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { reward: null },
|
|
||||||
errors: [{ message: 'Not Authorized!' }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
authenticatedUser = await administrator.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('badge for id does not exist', () => {
|
|
||||||
it('rejects with an informative error message', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables: { to: 'regular-user-id', from: 'non-existent-badge-id' },
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { reward: null },
|
|
||||||
errors: [{ message: "Couldn't find a badge with that id" }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('non-existent user', () => {
|
|
||||||
it('rejects with a telling error message', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables: { to: 'non-existent-user-id', from: 'indiegogo_en_rhino' },
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: { reward: null },
|
|
||||||
errors: [{ message: "Couldn't find a user with that id" }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rewards a badge to user', async () => {
|
|
||||||
const expected = {
|
|
||||||
data: {
|
|
||||||
reward: {
|
|
||||||
id: 'regular-user-id',
|
|
||||||
badges: [{ id: 'indiegogo_en_rhino' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject(
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rewards a second different badge to same user', async () => {
|
|
||||||
await Factory.build('badge', {
|
|
||||||
id: 'indiegogo_en_racoon',
|
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
|
||||||
})
|
|
||||||
const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }]
|
|
||||||
const expected = {
|
|
||||||
data: {
|
|
||||||
reward: {
|
|
||||||
id: 'regular-user-id',
|
|
||||||
badges: expect.arrayContaining(badges),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
await mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables: {
|
|
||||||
to: 'regular-user-id',
|
|
||||||
from: 'indiegogo_en_rhino',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables: {
|
|
||||||
to: 'regular-user-id',
|
|
||||||
from: 'indiegogo_en_racoon',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rewards the same badge as well to another user', async () => {
|
|
||||||
const expected = {
|
|
||||||
data: {
|
|
||||||
reward: {
|
|
||||||
id: 'regular-user-2-id',
|
|
||||||
badges: [{ id: 'indiegogo_en_rhino' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
await Factory.build(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
id: 'regular-user-2-id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'regular2@email.com',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables: {
|
|
||||||
to: 'regular-user-2-id',
|
|
||||||
from: 'indiegogo_en_rhino',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('creates no duplicate reward relationships', async () => {
|
|
||||||
await mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
await mutate({
|
|
||||||
mutation: rewardMutation,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
|
|
||||||
const userQuery = gql`
|
|
||||||
{
|
|
||||||
User(id: "regular-user-id") {
|
|
||||||
badgesCount
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const expected = {
|
|
||||||
data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] },
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(query({ query: userQuery })).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
authenticatedUser = moderator.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('rewards badge to user', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { reward: null },
|
|
||||||
errors: [{ message: 'Not Authorized!' }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unreward', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await regularUser.relateTo(badge, 'rewarded')
|
|
||||||
})
|
|
||||||
const expected = {
|
|
||||||
data: { unreward: { id: 'regular-user-id', badges: [] } },
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const unrewardMutation = gql`
|
|
||||||
mutation ($from: ID!, $to: ID!) {
|
|
||||||
unreward(badgeKey: $from, userId: $to) {
|
|
||||||
id
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('check test setup', () => {
|
|
||||||
it('user has one badge', async () => {
|
|
||||||
authenticatedUser = regularUser.toJson()
|
|
||||||
const userQuery = gql`
|
|
||||||
{
|
|
||||||
User(id: "regular-user-id") {
|
|
||||||
badgesCount
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const expected = {
|
|
||||||
data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] },
|
|
||||||
errors: undefined,
|
|
||||||
}
|
|
||||||
await expect(query({ query: userQuery })).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
authenticatedUser = null
|
|
||||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { unreward: null },
|
|
||||||
errors: [{ message: 'Not Authorized!' }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
authenticatedUser = await administrator.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes a badge from user', async () => {
|
|
||||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject(
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not crash when unrewarding multiple times', async () => {
|
|
||||||
await mutate({ mutation: unrewardMutation, variables })
|
|
||||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject(
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
authenticatedUser = await moderator.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('removes bage from user', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({
|
|
||||||
data: { unreward: null },
|
|
||||||
errors: [{ message: 'Not Authorized!' }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
|
||||||
|
|
||||||
import { getNeode } from '@db/neo4j'
|
|
||||||
|
|
||||||
const neode = getNeode()
|
|
||||||
|
|
||||||
const getUserAndBadge = async ({ badgeKey, userId }) => {
|
|
||||||
const user = await neode.first('User', 'id', userId)
|
|
||||||
const badge = await neode.first('Badge', 'id', badgeKey)
|
|
||||||
if (!user) throw new UserInputError("Couldn't find a user with that id")
|
|
||||||
if (!badge) throw new UserInputError("Couldn't find a badge with that id")
|
|
||||||
return { user, badge }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Mutation: {
|
|
||||||
reward: async (_object, params, context, _resolveInfo) => {
|
|
||||||
const { user, badge } = await getUserAndBadge(params)
|
|
||||||
await user.relateTo(badge, 'rewarded')
|
|
||||||
return user.toJson()
|
|
||||||
},
|
|
||||||
|
|
||||||
unreward: async (_object, params, context, _resolveInfo) => {
|
|
||||||
const { badgeKey, userId } = params
|
|
||||||
const { user } = await getUserAndBadge(params)
|
|
||||||
const session = context.driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((transaction) => {
|
|
||||||
return transaction.run(
|
|
||||||
`
|
|
||||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
|
||||||
DELETE reward
|
|
||||||
RETURN rewardedUser
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
badgeKey,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
return user.toJson()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -70,6 +70,36 @@ const updateOnlineStatus = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const setTrophyBadgeSelected = gql`
|
||||||
|
mutation ($slot: Int!, $badgeId: ID!) {
|
||||||
|
setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) {
|
||||||
|
badgeTrophiesCount
|
||||||
|
badgeTrophiesSelected {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophiesUnused {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophiesUnusedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const resetTrophyBadgesSelected = gql`
|
||||||
|
mutation {
|
||||||
|
resetTrophyBadgesSelected {
|
||||||
|
badgeTrophiesCount
|
||||||
|
badgeTrophiesSelected {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophiesUnused {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
badgeTrophiesUnusedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
|
|
||||||
@ -1070,3 +1100,279 @@ describe('updateOnlineStatus', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('setTrophyBadgeSelected', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await Factory.build('user', {
|
||||||
|
id: 'user',
|
||||||
|
role: 'user',
|
||||||
|
})
|
||||||
|
const badgeBear = await Factory.build('badge', {
|
||||||
|
id: 'trophy_bear',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Bear',
|
||||||
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
|
})
|
||||||
|
const badgePanda = await Factory.build('badge', {
|
||||||
|
id: 'trophy_panda',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Panda',
|
||||||
|
icon: '/img/badges/trophy_blue_panda.svg',
|
||||||
|
})
|
||||||
|
await Factory.build('badge', {
|
||||||
|
id: 'trophy_rabbit',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Rabbit',
|
||||||
|
icon: '/img/badges/trophy_blue_rabbit.svg',
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.relateTo(badgeBear, 'rewarded')
|
||||||
|
await user.relateTo(badgePanda, 'rewarded')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('not authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 0, badgeId: 'trophy_bear' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws Error when slot is out of bound', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: -1, badgeId: 'trophy_bear' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Invalid slot! There is only 9 badge-slots to fill',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 9, badgeId: 'trophy_bear' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Invalid slot! There is only 9 badge-slots to fill',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws Error when badge was not rewarded to user', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 0, badgeId: 'trophy_rabbit' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Error: You cannot set badges not rewarded to you.',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws Error when badge is unknown', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 0, badgeId: 'trophy_unknown' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Error: You cannot set badges not rewarded to you.',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the user with badges set on slots', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 0, badgeId: 'trophy_bear' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
setTrophyBadgeSelected: {
|
||||||
|
badgeTrophiesCount: 2,
|
||||||
|
badgeTrophiesSelected: [
|
||||||
|
{
|
||||||
|
id: 'trophy_bear',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
badgeTrophiesUnused: [
|
||||||
|
{
|
||||||
|
id: 'trophy_panda',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
badgeTrophiesUnusedCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 5, badgeId: 'trophy_panda' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
setTrophyBadgeSelected: {
|
||||||
|
badgeTrophiesCount: 2,
|
||||||
|
badgeTrophiesSelected: [
|
||||||
|
{
|
||||||
|
id: 'trophy_bear',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
id: 'trophy_panda',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
badgeTrophiesUnused: [],
|
||||||
|
badgeTrophiesUnusedCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetTrophyBadgesSelected', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await Factory.build('user', {
|
||||||
|
id: 'user',
|
||||||
|
role: 'user',
|
||||||
|
})
|
||||||
|
const badgeBear = await Factory.build('badge', {
|
||||||
|
id: 'trophy_bear',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Bear',
|
||||||
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
|
})
|
||||||
|
const badgePanda = await Factory.build('badge', {
|
||||||
|
id: 'trophy_panda',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Panda',
|
||||||
|
icon: '/img/badges/trophy_blue_panda.svg',
|
||||||
|
})
|
||||||
|
await Factory.build('badge', {
|
||||||
|
id: 'trophy_rabbit',
|
||||||
|
type: 'trophy',
|
||||||
|
description: 'You earned a Rabbit',
|
||||||
|
icon: '/img/badges/trophy_blue_rabbit.svg',
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.relateTo(badgeBear, 'rewarded')
|
||||||
|
await user.relateTo(badgePanda, 'rewarded')
|
||||||
|
|
||||||
|
await mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 0, badgeId: 'trophy_bear' },
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: setTrophyBadgeSelected,
|
||||||
|
variables: { slot: 5, badgeId: 'trophy_panda' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('not authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the user with no profile badges badges set', async () => {
|
||||||
|
await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
resetTrophyBadgesSelected: {
|
||||||
|
badgeTrophiesCount: 2,
|
||||||
|
badgeTrophiesSelected: [null, null, null, null, null, null, null, null, null],
|
||||||
|
badgeTrophiesUnused: [
|
||||||
|
{
|
||||||
|
id: 'trophy_panda',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trophy_bear',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
badgeTrophiesUnusedCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { UserInputError, ForbiddenError } from 'apollo-server'
|
import { UserInputError, ForbiddenError } from 'apollo-server'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
|
||||||
|
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
|
||||||
import { getNeode } from '@db/neo4j'
|
import { getNeode } from '@db/neo4j'
|
||||||
|
|
||||||
import log from './helpers/databaseLogger'
|
import log from './helpers/databaseLogger'
|
||||||
@ -381,6 +382,73 @@ export default {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
setTrophyBadgeSelected: async (_object, args, context, _resolveInfo) => {
|
||||||
|
const { slot, badgeId } = args
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
if (slot >= TROPHY_BADGES_SELECTED_MAX || slot < 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid slot! There is only ${TROPHY_BADGES_SELECTED_MAX} badge-slots to fill`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const query = session.writeTransaction(async (transaction) => {
|
||||||
|
const result = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId})
|
||||||
|
OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge)
|
||||||
|
OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge)
|
||||||
|
DELETE badgeRelation, slotRelation
|
||||||
|
MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge)
|
||||||
|
RETURN user {.*}
|
||||||
|
`,
|
||||||
|
{ userId, badgeId, slot },
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('user'))[0]
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const user = await query
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('You cannot set badges not rewarded to you.')
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetTrophyBadgesSelected: async (_object, _args, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const query = session.writeTransaction(async (transaction) => {
|
||||||
|
const result = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})
|
||||||
|
OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge)
|
||||||
|
DELETE relation
|
||||||
|
RETURN user {.*}
|
||||||
|
`,
|
||||||
|
{ userId },
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('user'))[0]
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
return await query
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
emailNotificationSettings: async (parent, params, context, resolveInfo) => {
|
emailNotificationSettings: async (parent, params, context, resolveInfo) => {
|
||||||
@ -438,6 +506,87 @@ export default {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
badgeTrophiesSelected: async (parent, _params, context, _resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const query = session.readTransaction(async (transaction) => {
|
||||||
|
const result = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge)
|
||||||
|
WITH relation, badge
|
||||||
|
ORDER BY relation.slot ASC
|
||||||
|
RETURN relation.slot as slot, badge {.*}
|
||||||
|
`,
|
||||||
|
{ parent },
|
||||||
|
)
|
||||||
|
return result.records
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const badgesSelected = await query
|
||||||
|
const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(null)
|
||||||
|
badgesSelected.map((record) => {
|
||||||
|
result[record.get('slot')] = record.get('badge')
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
badgeTrophiesUnused: async (_parent, _params, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const query = session.writeTransaction(async (transaction) => {
|
||||||
|
const result = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge)
|
||||||
|
WHERE NOT (user)-[:SELECTED]-(badge)
|
||||||
|
RETURN badge {.*}
|
||||||
|
`,
|
||||||
|
{ userId },
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('badge'))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
return await query
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
badgeTrophiesUnusedCount: async (_parent, _params, context, _resolveInfo) => {
|
||||||
|
const {
|
||||||
|
user: { id: userId },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const query = session.writeTransaction(async (transaction) => {
|
||||||
|
const result = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge)
|
||||||
|
WHERE NOT (user)-[:SELECTED]-(badge)
|
||||||
|
RETURN toString(COUNT(badge)) as count
|
||||||
|
`,
|
||||||
|
{ userId },
|
||||||
|
)
|
||||||
|
return result.records.map((record) => record.get('count'))[0]
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
return await query
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
...Resolver('User', {
|
...Resolver('User', {
|
||||||
undefinedToNull: [
|
undefinedToNull: [
|
||||||
'actorId',
|
'actorId',
|
||||||
@ -471,13 +620,14 @@ export default {
|
|||||||
'-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
'-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||||
shoutedCount:
|
shoutedCount:
|
||||||
'-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
'-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||||
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
badgeTrophiesCount: '<-[:REWARDED]-(related:Badge)',
|
||||||
},
|
},
|
||||||
hasOne: {
|
hasOne: {
|
||||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||||
invitedBy: '<-[:INVITED]-(related:User)',
|
invitedBy: '<-[:INVITED]-(related:User)',
|
||||||
location: '-[:IS_IN]->(related:Location)',
|
location: '-[:IS_IN]->(related:Location)',
|
||||||
redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)',
|
redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)',
|
||||||
|
badgeVerification: '<-[:VERIFIES]-(related:Badge)',
|
||||||
},
|
},
|
||||||
hasMany: {
|
hasMany: {
|
||||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||||
@ -488,7 +638,7 @@ export default {
|
|||||||
comments: '-[:WROTE]->(related:Comment)',
|
comments: '-[:WROTE]->(related:Comment)',
|
||||||
shouted: '-[:SHOUTED]->(related:Post)',
|
shouted: '-[:SHOUTED]->(related:Post)',
|
||||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||||
badges: '<-[:REWARDED]-(related:Badge)',
|
badgeTrophies: '<-[:REWARDED]-(related:Badge)',
|
||||||
inviteCodes: '-[:GENERATED]->(related:InviteCode)',
|
inviteCodes: '-[:GENERATED]->(related:InviteCode)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,22 +1,17 @@
|
|||||||
type Badge {
|
type Badge {
|
||||||
id: ID!
|
id: ID!
|
||||||
type: BadgeType!
|
type: BadgeType!
|
||||||
status: BadgeStatus!
|
|
||||||
icon: String!
|
icon: String!
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
description: String!
|
||||||
|
|
||||||
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
||||||
}
|
verifies: [User]! @relation(name: "VERIFIES", direction: "OUT")
|
||||||
|
|
||||||
enum BadgeStatus {
|
|
||||||
permanent
|
|
||||||
temporary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BadgeType {
|
enum BadgeType {
|
||||||
role
|
verification
|
||||||
crowdfunding
|
trophy
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@ -24,6 +19,7 @@ type Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
reward(badgeKey: ID!, userId: ID!): User
|
setVerificationBadge(badgeId: ID!, userId: ID!): User
|
||||||
unreward(badgeKey: ID!, userId: ID!): User
|
rewardTrophyBadge(badgeId: ID!, userId: ID!): User
|
||||||
|
revokeBadge(badgeId: ID!, userId: ID!): User
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,8 +125,12 @@ type User {
|
|||||||
|
|
||||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
badgeVerification: Badge @relation(name: "VERIFIES", direction: "IN")
|
||||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||||
|
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||||
|
badgeTrophiesSelected: [Badge]! @neo4j_ignore
|
||||||
|
badgeTrophiesUnused: [Badge]! @neo4j_ignore
|
||||||
|
badgeTrophiesUnusedCount: Int! @neo4j_ignore
|
||||||
|
|
||||||
emotions: [EMOTED]
|
emotions: [EMOTED]
|
||||||
|
|
||||||
@ -247,4 +251,7 @@ type Mutation {
|
|||||||
|
|
||||||
# Get a JWT Token for the given Email and password
|
# Get a JWT Token for the given Email and password
|
||||||
login(email: String!, password: String!): String!
|
login(email: String!, password: String!): String!
|
||||||
|
|
||||||
|
setTrophyBadgeSelected(slot: Int!, badgeId: ID!): User
|
||||||
|
resetTrophyBadgesSelected: User
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ const comment = {
|
|||||||
location: null,
|
location: null,
|
||||||
badges: [
|
badges: [
|
||||||
{
|
{
|
||||||
id: 'indiegogo_en_bear',
|
id: 'trophy_bear',
|
||||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
__typename: 'Badge',
|
__typename: 'Badge',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -33,8 +33,8 @@ export const post = {
|
|||||||
badges: [
|
badges: [
|
||||||
{
|
{
|
||||||
id: 'b4',
|
id: 'b4',
|
||||||
key: 'indiegogo_en_bear',
|
key: 'trophy_bear',
|
||||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
__typename: 'Badge',
|
__typename: 'Badge',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -41,8 +41,8 @@ export const user = {
|
|||||||
commentedCount: 3,
|
commentedCount: 3,
|
||||||
badges: [
|
badges: [
|
||||||
{
|
{
|
||||||
id: 'indiegogo_en_bear',
|
id: 'trophy_bear',
|
||||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
icon: '/img/badges/trophy_blue_bear.svg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
2
webapp/constants/badges.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js`
|
||||||
|
export const TROPHY_BADGES_SELECTED_MAX = 9
|
||||||
@ -29,7 +29,7 @@ export default (i18n) => {
|
|||||||
commentedCount
|
commentedCount
|
||||||
followedByCount
|
followedByCount
|
||||||
followedByCurrentUser
|
followedByCurrentUser
|
||||||
badges {
|
badgeTrophies {
|
||||||
id
|
id
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const locationFragment = (lang) => gql`
|
|||||||
|
|
||||||
export const badgesFragment = gql`
|
export const badgesFragment = gql`
|
||||||
fragment badges on User {
|
fragment badges on User {
|
||||||
badges {
|
badgeTrophies {
|
||||||
id
|
id
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,8 @@
|
|||||||
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
|
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<ds-space v-if="user.badges && user.badges.length" margin="x-small">
|
<ds-space v-if="user.badgeTrophies && user.badgeTrophies.length" margin="x-small">
|
||||||
<hc-badges :badges="user.badges" />
|
<hc-badges :badges="user.badgeTrophies" />
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<ds-flex>
|
<ds-flex>
|
||||||
<ds-flex-item>
|
<ds-flex-item>
|
||||||
|
|||||||