From a1967815bf43402a52241f36c55e33c79f524eac Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 7 Jan 2021 13:30:11 +0100 Subject: [PATCH 01/16] further invite code --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/models/InviteCode.js | 19 ++++++ backend/src/models/User.js | 12 ++++ backend/src/models/index.js | 1 + .../resolvers/helpers/generateInviteCode.js | 5 ++ backend/src/schema/resolvers/inviteCodes.js | 66 +++++++++++++++++++ backend/src/schema/resolvers/users.js | 2 + backend/src/schema/types/type/InviteCode.gql | 14 ++++ backend/src/schema/types/type/User.gql | 5 +- 9 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 backend/src/models/InviteCode.js create mode 100644 backend/src/schema/resolvers/helpers/generateInviteCode.js create mode 100644 backend/src/schema/resolvers/inviteCodes.js create mode 100644 backend/src/schema/types/type/InviteCode.gql diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index f4f8c654b..75caa3c0e 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -149,6 +149,7 @@ export default shield( pinPost: isAdmin, unpinPost: isAdmin, UpdateDonations: isAdmin, + CreateInviteCode: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/InviteCode.js b/backend/src/models/InviteCode.js new file mode 100644 index 000000000..da9d3bb48 --- /dev/null +++ b/backend/src/models/InviteCode.js @@ -0,0 +1,19 @@ +export default { + code: { type: 'string', primary: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + uses: { type: 'int', default: () => 0 }, + maxUses: { type: 'int', default: () => 1 }, + active: { type: 'boolean', default: () => true }, + createdBy: { + type: 'relationship', + relationship: 'CREATED', + target: 'User', + direction: 'in', + }, + usedBy: { + type: 'relationship', + relationship: 'USED', + target: 'User', + direction: 'in', + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js index ae7e1ae8c..e9e21e856 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -100,6 +100,18 @@ export default { target: 'User', direction: 'in', }, + createdInvite: { + type: 'relationship', + relationship: 'CREATED', + target: 'InviteCode', + direction: 'out', + }, + usedInvite: { + type: 'relationship', + relationship: 'USED', + target: 'InviteCode', + direction: 'out', + }, termsAndConditionsAgreedVersion: { type: 'string', allow: [null], diff --git a/backend/src/models/index.js b/backend/src/models/index.js index c53ef89ab..8d6a021ab 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -15,4 +15,5 @@ export default { Donations: require('./Donations.js').default, Report: require('./Report.js').default, Migration: require('./Migration.js').default, + InviteCode: require('./InviteCode.js').default, } diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.js b/backend/src/schema/resolvers/helpers/generateInviteCode.js new file mode 100644 index 000000000..1d42c2d5d --- /dev/null +++ b/backend/src/schema/resolvers/helpers/generateInviteCode.js @@ -0,0 +1,5 @@ +export default function generateInviteCode() { + return Array.from({length: 6}, (n = Math.floor(Math.random() * 36)) => { + return String.fromCharCode(n > 9 ? n + 55 : n + 48) + }).join('') +} diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js new file mode 100644 index 000000000..450339add --- /dev/null +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -0,0 +1,66 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import generateInvieCode from './helpers/generateInviteCode' +import Resolver from './helpers/Resolver' + +const uniqueInviteCode = async (session, code) => { + return session.readTransaction(async (txc) => { + const result = await txc.run( + `MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, + { code }, + ) + return parseInt(String(result.records[0].get('count'))) === 0 + }) +} + +export default { + Mutation: { + CreateInviteCode: async (_parent, args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + const session = context.driver.session() + let code = generateInvieCode() + let response + while(!await uniqueInviteCode(session, code)) { + code = generateInvieCode() + } + const writeTxResultPromise = session.writeTransaction(async (txc) => { + const result = await txc.run( + `MATCH (user:User {id: $userId}) + MERGE (user)-[:CREATED]->(ic:InviteCode { + code: $code, + createdAt: toString(datetime()), + uses: $uses, + maxUses: $maxUses, + active: true + }) RETURN ic AS inviteCode`, + { + userId, + code, + maxUses: args.maxUses, + uses: 0, + }, + ) + return result.records.map((record) => record.get('inviteCode').properties) + }) + try { + const txResult = await writeTxResultPromise + console.log(txResult) + response = txResult[0] + } finally { + session.close() + } + return response + } + }, + InviteCode: { + ...Resolver('InviteCode', { + hasOne: { + createdBy: '<-[:CREATED]-(related:User)', + }, + hasMany: { + usedBy: '<-[:USED]-(related:User)', + }, + }), + }, +} diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index e276968e5..c718187c1 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -293,6 +293,7 @@ export default { avatar: '-[:AVATAR_IMAGE]->(related:Image)', invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', + usedInviteCode: '-[:USED]->(related:InviteCode)', }, hasMany: { followedBy: '<-[:FOLLOWS]-(related:User)', @@ -304,6 +305,7 @@ export default { shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', badges: '<-[:REWARDED]-(related:Badge)', + inviteCodes: '-[:CREATED]->(related:InviteCode)', }, }), }, diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql new file mode 100644 index 000000000..b20235620 --- /dev/null +++ b/backend/src/schema/types/type/InviteCode.gql @@ -0,0 +1,14 @@ +type InviteCode { + code: ID! + createdAt: String + uses: Int! + maxUses: Int! + createdBy: User @relation(name: "CREATED", direction: "IN") + usedBy: [User] @relation(name: "USED", direction: "IN") + active: Boolean! +} + + +type Mutation { + CreateInviteCode(maxUses: Int = 1): InviteCode +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index e6e7191c5..bbee746b9 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -56,6 +56,9 @@ type User { followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + inviteCodes: [InviteCode] @relation(name: "CREATED", direction: "OUT") + usedInviteCode: InviteCode @relation(name: "USED", direction: "OUT") + # Is the currently logged in user following that user? followedByCurrentUser: Boolean! @cypher( statement: """ @@ -83,7 +86,7 @@ type User { RETURN COUNT(user) >= 1 """ ) - + # contributions: [WrittenPost]! # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! # @cypher( From dd6cafed4cae6055ea6510b4d56860b093eb85b6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 11 Jan 2021 21:25:02 +0100 Subject: [PATCH 02/16] basic specs for inviteCodes --- .../src/middleware/permissionsMiddleware.js | 2 +- backend/src/models/User.js | 8 +- .../resolvers/helpers/generateInviteCode.js | 4 +- backend/src/schema/resolvers/inviteCodes.js | 37 +++--- .../src/schema/resolvers/inviteCodes.spec.js | 113 ++++++++++++++++++ backend/src/schema/resolvers/users.js | 4 +- backend/src/schema/types/type/InviteCode.gql | 10 +- backend/src/schema/types/type/User.gql | 4 +- 8 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 backend/src/schema/resolvers/inviteCodes.spec.js diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 75caa3c0e..ac709279d 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -149,7 +149,7 @@ export default shield( pinPost: isAdmin, unpinPost: isAdmin, UpdateDonations: isAdmin, - CreateInviteCode: isAuthenticated, + GenerateInviteCode: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/User.js b/backend/src/models/User.js index e9e21e856..6cfd22268 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -100,15 +100,15 @@ export default { target: 'User', direction: 'in', }, - createdInvite: { + inviteCodes: { type: 'relationship', - relationship: 'CREATED', + relationship: 'GENERATED', target: 'InviteCode', direction: 'out', }, - usedInvite: { + redeemedInviteCode: { type: 'relationship', - relationship: 'USED', + relationship: 'REDEEMED', target: 'InviteCode', direction: 'out', }, diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.js b/backend/src/schema/resolvers/helpers/generateInviteCode.js index 1d42c2d5d..2b3713e3d 100644 --- a/backend/src/schema/resolvers/helpers/generateInviteCode.js +++ b/backend/src/schema/resolvers/helpers/generateInviteCode.js @@ -1,5 +1,5 @@ export default function generateInviteCode() { - return Array.from({length: 6}, (n = Math.floor(Math.random() * 36)) => { + return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => { return String.fromCharCode(n > 9 ? n + 55 : n + 48) - }).join('') + }).join('') } diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js index 450339add..7a39bc081 100644 --- a/backend/src/schema/resolvers/inviteCodes.js +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -1,65 +1,60 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' import generateInvieCode from './helpers/generateInviteCode' import Resolver from './helpers/Resolver' const uniqueInviteCode = async (session, code) => { return session.readTransaction(async (txc) => { - const result = await txc.run( - `MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, - { code }, - ) - return parseInt(String(result.records[0].get('count'))) === 0 + const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, { + code, + }) + return parseInt(String(result.records[0].get('count'))) === 0 }) } export default { Mutation: { - CreateInviteCode: async (_parent, args, context, _resolveInfo) => { + GenerateInviteCode: async (_parent, args, context, _resolveInfo) => { const { user: { id: userId }, } = context const session = context.driver.session() let code = generateInvieCode() let response - while(!await uniqueInviteCode(session, code)) { + while (!(await uniqueInviteCode(session, code))) { code = generateInvieCode() } const writeTxResultPromise = session.writeTransaction(async (txc) => { const result = await txc.run( `MATCH (user:User {id: $userId}) - MERGE (user)-[:CREATED]->(ic:InviteCode { - code: $code, - createdAt: toString(datetime()), - uses: $uses, - maxUses: $maxUses, - active: true - }) RETURN ic AS inviteCode`, + MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code }) + ON CREATE SET + ic.createdAt = toString(datetime()), + ic.expiresAt = $expiresAt + RETURN ic AS inviteCode`, { userId, code, - maxUses: args.maxUses, - uses: 0, + expiresAt: args.expiresAt, }, ) return result.records.map((record) => record.get('inviteCode').properties) }) try { const txResult = await writeTxResultPromise - console.log(txResult) response = txResult[0] } finally { session.close() } return response - } + }, }, InviteCode: { ...Resolver('InviteCode', { + undefinedToNull: ['expiresAt'], hasOne: { - createdBy: '<-[:CREATED]-(related:User)', + generatedBy: '<-[:GENERATED]-(related:User)', }, hasMany: { - usedBy: '<-[:USED]-(related:User)', + redeemedBy: '<-[:REDEEMED]-(related:User)', }, }), }, diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js new file mode 100644 index 000000000..04a8d2a5a --- /dev/null +++ b/backend/src/schema/resolvers/inviteCodes.spec.js @@ -0,0 +1,113 @@ +import Factory, { cleanDatabase } from '../../db/factories' +import { getDriver } from '../../db/neo4j' +import { gql } from '../../helpers/jest' +import createServer from '../../server' +import { createTestClient } from 'apollo-server-testing' + +let user +// let query +let mutate + +const driver = getDriver() + +const generateInviteCodeMutation = gql` + mutation($expiresAt: String = null) { + GenerateInviteCode(expiresAt: $expiresAt) { + code + createdAt + expiresAt + } + } +` + +beforeAll(async () => { + await cleanDatabase() + const { server } = createServer({ + context: () => { + return { + driver, + user, + } + }, + }) + // query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + await cleanDatabase() +}) + +describe('inviteCodes', () => { + describe('generate invite code', () => { + describe('as unauthenticated user', () => { + it('returns permission denied error', async () => { + await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }), + ]), + data: { + GenerateInviteCode: null, + }, + }), + ) + }) + }) + + describe('as authenticated user', () => { + beforeAll(async () => { + const authenticatedUser = await Factory.build( + 'user', + { + role: 'user', + }, + { + email: 'user@example.org', + password: '1234', + }, + ) + user = await authenticatedUser.toJson() + }) + + it('generates an invite code without expiresAt', async () => { + await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( + expect.objectContaining({ + errors: undefined, + data: { + GenerateInviteCode: { + code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), + expiresAt: null, + createdAt: expect.any(String), + }, + }, + }), + ) + }) + + it('generates an invite code with expiresAt', async () => { + const nextWeek = new Date() + nextWeek.setDate(nextWeek.getDate() + 7) + await expect( + mutate({ + mutation: generateInviteCodeMutation, + variables: { expiresAt: nextWeek.toISOString() }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: undefined, + data: { + GenerateInviteCode: { + code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), + expiresAt: nextWeek.toISOString(), + createdAt: expect.any(String), + }, + }, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index c718187c1..edc81482f 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -293,7 +293,7 @@ export default { avatar: '-[:AVATAR_IMAGE]->(related:Image)', invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', - usedInviteCode: '-[:USED]->(related:InviteCode)', + redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)', }, hasMany: { followedBy: '<-[:FOLLOWS]-(related:User)', @@ -305,7 +305,7 @@ export default { shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', badges: '<-[:REWARDED]-(related:Badge)', - inviteCodes: '-[:CREATED]->(related:InviteCode)', + inviteCodes: '-[:GENERATED]->(related:InviteCode)', }, }), }, diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql index b20235620..8ed7fbcab 100644 --- a/backend/src/schema/types/type/InviteCode.gql +++ b/backend/src/schema/types/type/InviteCode.gql @@ -1,14 +1,12 @@ type InviteCode { code: ID! createdAt: String - uses: Int! - maxUses: Int! - createdBy: User @relation(name: "CREATED", direction: "IN") - usedBy: [User] @relation(name: "USED", direction: "IN") - active: Boolean! + generatedBy: User @relation(name: "GENERATED", direction: "IN") + redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN") + expiresAt: String } type Mutation { - CreateInviteCode(maxUses: Int = 1): InviteCode + GenerateInviteCode(expiresAt: String = null): InviteCode } diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index bbee746b9..f5a561f65 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -56,8 +56,8 @@ type User { followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") - inviteCodes: [InviteCode] @relation(name: "CREATED", direction: "OUT") - usedInviteCode: InviteCode @relation(name: "USED", direction: "OUT") + inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT") + redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT") # Is the currently logged in user following that user? followedByCurrentUser: Boolean! @cypher( From 9449a55d420496921a25317fa559087b2a1940cb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 12 Jan 2021 16:43:34 +0100 Subject: [PATCH 03/16] qyery for generated invite codes --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/inviteCodes.js | 29 ++- .../src/schema/resolvers/inviteCodes.spec.js | 170 +++++++++++------- backend/src/schema/types/type/InviteCode.gql | 4 + 4 files changed, 133 insertions(+), 71 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index ac709279d..27eb3aa08 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -106,6 +106,7 @@ export default shield( notifications: isAuthenticated, Donations: isAuthenticated, userData: isAuthenticated, + MyInviteCodes: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js index 7a39bc081..0f7666dc2 100644 --- a/backend/src/schema/resolvers/inviteCodes.js +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -11,6 +11,30 @@ const uniqueInviteCode = async (session, code) => { } export default { + Query: { + MyInviteCodes: async (_parent, args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + const session = context.driver.session() + const readTxResultPromise = session.readTransaction(async (txc) => { + const result = await txc.run( + `MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode) + RETURN properties(ic) AS inviteCodes`, + { + userId, + }, + ) + return result.records.map((record) => record.get('inviteCodes')) + }) + try { + const txResult = await readTxResultPromise + return txResult + } finally { + session.close() + } + }, + }, Mutation: { GenerateInviteCode: async (_parent, args, context, _resolveInfo) => { const { @@ -18,7 +42,6 @@ export default { } = context const session = context.driver.session() let code = generateInvieCode() - let response while (!(await uniqueInviteCode(session, code))) { code = generateInvieCode() } @@ -40,15 +63,15 @@ export default { }) try { const txResult = await writeTxResultPromise - response = txResult[0] + return txResult[0] } finally { session.close() } - return response }, }, InviteCode: { ...Resolver('InviteCode', { + idAttribute: 'code', undefinedToNull: ['expiresAt'], hasOne: { generatedBy: '<-[:GENERATED]-(related:User)', diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js index 04a8d2a5a..f0fee4b8d 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.js +++ b/backend/src/schema/resolvers/inviteCodes.spec.js @@ -5,7 +5,7 @@ import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' let user -// let query +let query let mutate const driver = getDriver() @@ -20,6 +20,16 @@ const generateInviteCodeMutation = gql` } ` +const myInviteCodesQuery = gql` + query { + MyInviteCodes { + code + createdAt + expiresAt + } + } +` + beforeAll(async () => { await cleanDatabase() const { server } = createServer({ @@ -30,7 +40,7 @@ beforeAll(async () => { } }, }) - // query = createTestClient(server).query + query = createTestClient(server).query mutate = createTestClient(server).mutate }) @@ -39,75 +49,99 @@ afterAll(async () => { }) describe('inviteCodes', () => { - describe('generate invite code', () => { - describe('as unauthenticated user', () => { - it('returns permission denied error', async () => { - await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( - expect.objectContaining({ - errors: expect.arrayContaining([ - expect.objectContaining({ - extensions: { code: 'INTERNAL_SERVER_ERROR' }, - }), - ]), - data: { - GenerateInviteCode: null, - }, - }), - ) - }) + describe('as unauthenticated user', () => { + it('cannot generate invite codes', async () => { + await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }), + ]), + data: { + GenerateInviteCode: null, + }, + }), + ) }) - describe('as authenticated user', () => { - beforeAll(async () => { - const authenticatedUser = await Factory.build( - 'user', - { - role: 'user', + it('cannot query invite codes', async () => { + await expect(query({ query: myInviteCodesQuery })).resolves.toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }), + ]), + data: { + MyInviteCodes: null, }, - { - email: 'user@example.org', - password: '1234', - }, - ) - user = await authenticatedUser.toJson() - }) - - it('generates an invite code without expiresAt', async () => { - await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( - expect.objectContaining({ - errors: undefined, - data: { - GenerateInviteCode: { - code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), - expiresAt: null, - createdAt: expect.any(String), - }, - }, - }), - ) - }) - - it('generates an invite code with expiresAt', async () => { - const nextWeek = new Date() - nextWeek.setDate(nextWeek.getDate() + 7) - await expect( - mutate({ - mutation: generateInviteCodeMutation, - variables: { expiresAt: nextWeek.toISOString() }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: undefined, - data: { - GenerateInviteCode: { - code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), - expiresAt: nextWeek.toISOString(), - createdAt: expect.any(String), - }, - }, - }), - ) - }) + }), + ) }) }) + + describe('as authenticated user', () => { + beforeAll(async () => { + const authenticatedUser = await Factory.build( + 'user', + { + role: 'user', + }, + { + email: 'user@example.org', + password: '1234', + }, + ) + user = await authenticatedUser.toJson() + }) + + it('generates an invite code without expiresAt', async () => { + await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( + expect.objectContaining({ + errors: undefined, + data: { + GenerateInviteCode: { + code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), + expiresAt: null, + createdAt: expect.any(String), + }, + }, + }), + ) + }) + + it('generates an invite code with expiresAt', async () => { + const nextWeek = new Date() + nextWeek.setDate(nextWeek.getDate() + 7) + await expect( + mutate({ + mutation: generateInviteCodeMutation, + variables: { expiresAt: nextWeek.toISOString() }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: undefined, + data: { + GenerateInviteCode: { + code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), + expiresAt: nextWeek.toISOString(), + createdAt: expect.any(String), + }, + }, + }), + ) + }) + + let inviteCodes + + it('returns the created invite codes when queried', async () => { + const response = await query({ query: myInviteCodesQuery }) + inviteCodes = response.data.MyInviteCodes + expect(inviteCodes).toHaveLength(2) + }) + + // const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null) + // const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null) + }) }) diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql index 8ed7fbcab..00ca99b75 100644 --- a/backend/src/schema/types/type/InviteCode.gql +++ b/backend/src/schema/types/type/InviteCode.gql @@ -10,3 +10,7 @@ type InviteCode { type Mutation { GenerateInviteCode(expiresAt: String = null): InviteCode } + +type Query { + MyInviteCodes: [InviteCode] +} From 0c141b631f5791791b0c6f47fc99e3f0d87ddda1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 12 Jan 2021 20:11:47 +0100 Subject: [PATCH 04/16] factory and seeding for invite codes --- backend/src/db/factories.js | 25 +++++++++++++++++++++++++ backend/src/db/seed.js | 10 ++++++++++ backend/src/models/InvitationCode.js | 16 ---------------- backend/src/models/InviteCode.js | 12 +++++------- 4 files changed, 40 insertions(+), 23 deletions(-) delete mode 100644 backend/src/models/InvitationCode.js diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 1ebf063ff..ac77cdb31 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -5,6 +5,7 @@ import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' import { getDriver, getNeode } from './neo4j' import CONFIG from '../config/index.js' +import generateInvieCode from '../schema/resolvers/helpers/generateInviteCode.js' const neode = getNeode() @@ -216,6 +217,30 @@ Factory.define('unverifiedEmailAddress') return neode.create('UnverifiedEmailAddress', buildObject) }) +const inviteCodeDefaults = { + code: generateInvieCode(), + createdAt: () => new Date().toISOString(), + expiresAt: () => null, +} + +Factory.define('inviteCode') + .attr(inviteCodeDefaults) + .option('generatedById', null) + .option('generatedBy', ['generatedById'], (generatedById) => { + if (generatedById) return neode.find('User', generatedById) + return Factory.build('user') + }) + .after(async (buildObject, options) => { + const [inviteCode, generatedBy] = await Promise.all([ + neode.create('InviteCode', buildObject), + options.generatedBy, + ]) + await Promise.all([ + inviteCode.relateTo(generatedBy, 'generated'), + ]) + return inviteCode + }) + Factory.define('location') .attrs({ name: 'Germany', diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 685b5ef0e..f58a69e97 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -541,6 +541,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ), ]) + await Factory.build( + 'inviteCode', + { + code: 'AAAAAA', + }, + { + generatedBy: jennyRostock, + }, + ) + authenticatedUser = await louie.toJson() const mention1 = 'Hey @jenny-rostock, what\'s up?' diff --git a/backend/src/models/InvitationCode.js b/backend/src/models/InvitationCode.js deleted file mode 100644 index 138289faf..000000000 --- a/backend/src/models/InvitationCode.js +++ /dev/null @@ -1,16 +0,0 @@ -export default { - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - token: { type: 'string', primary: true, token: true }, - generatedBy: { - type: 'relationship', - relationship: 'GENERATED', - target: 'User', - direction: 'in', - }, - activated: { - type: 'relationship', - relationship: 'ACTIVATED', - target: 'EmailAddress', - direction: 'out', - }, -} diff --git a/backend/src/models/InviteCode.js b/backend/src/models/InviteCode.js index da9d3bb48..7204f1b38 100644 --- a/backend/src/models/InviteCode.js +++ b/backend/src/models/InviteCode.js @@ -1,18 +1,16 @@ export default { code: { type: 'string', primary: true }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - uses: { type: 'int', default: () => 0 }, - maxUses: { type: 'int', default: () => 1 }, - active: { type: 'boolean', default: () => true }, - createdBy: { + expiresAt: { type: 'string', isoDate: true, default: null }, + generated: { type: 'relationship', - relationship: 'CREATED', + relationship: 'GENERATED', target: 'User', direction: 'in', }, - usedBy: { + redeemed: { type: 'relationship', - relationship: 'USED', + relationship: 'REDEEMED', target: 'User', direction: 'in', }, From f20e17dcc1a97a861531e3413d36318fa2748126 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 12 Jan 2021 21:25:25 +0100 Subject: [PATCH 05/16] query for validation of invite code --- backend/src/db/factories.js | 4 +- backend/src/db/seed.js | 2 +- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/inviteCodes.js | 25 ++++++++++++ .../src/schema/resolvers/inviteCodes.spec.js | 39 ++++++++++++++++++- backend/src/schema/types/type/InviteCode.gql | 3 +- 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index ac77cdb31..8f1d3c718 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -235,9 +235,7 @@ Factory.define('inviteCode') neode.create('InviteCode', buildObject), options.generatedBy, ]) - await Promise.all([ - inviteCode.relateTo(generatedBy, 'generated'), - ]) + await Promise.all([inviteCode.relateTo(generatedBy, 'generated')]) return inviteCode }) diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index f58a69e97..d7bd5c73b 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -550,7 +550,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] generatedBy: jennyRostock, }, ) - + authenticatedUser = await louie.toJson() const mention1 = 'Hey @jenny-rostock, what\'s up?' diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 27eb3aa08..ddf12598b 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -107,6 +107,7 @@ export default shield( Donations: isAuthenticated, userData: isAuthenticated, MyInviteCodes: isAuthenticated, + isValidInviteCode: allow, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js index 0f7666dc2..ec5517909 100644 --- a/backend/src/schema/resolvers/inviteCodes.js +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -34,6 +34,31 @@ export default { session.close() } }, + isValidInviteCode: async (_parent, args, context, _resolveInfo) => { + const { code } = args + if (!code) return false + const session = context.driver.session() + const readTxResultPromise = session.readTransaction(async (txc) => { + const result = await txc.run( + `MATCH (ic:InviteCode { code: $code }) + RETURN + CASE + WHEN ic.expiresAt IS NULL THEN true + WHEN datetime(ic.expiresAt) >= datetime() THEN true + ELSE false END AS result`, + { + code, + }, + ) + return result.records.map((record) => record.get('result')) + }) + try { + const txResult = await readTxResultPromise + return !!txResult[0] + } finally { + session.close() + } + }, }, Mutation: { GenerateInviteCode: async (_parent, args, context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js index f0fee4b8d..9fa3e188b 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.js +++ b/backend/src/schema/resolvers/inviteCodes.spec.js @@ -30,6 +30,12 @@ const myInviteCodesQuery = gql` } ` +const isValidInviteCodeQuery = gql` + query($code: ID) { + isValidInviteCode(code: $code) + } +` + beforeAll(async () => { await cleanDatabase() const { server } = createServer({ @@ -141,7 +147,36 @@ describe('inviteCodes', () => { expect(inviteCodes).toHaveLength(2) }) - // const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null) - // const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null) + it('does not returns the created invite codes of other users when queried', async () => { + await Factory.build('inviteCode') + const response = await query({ query: myInviteCodesQuery }) + inviteCodes = response.data.MyInviteCodes + expect(inviteCodes).toHaveLength(2) + }) + + it('validates an invite code without expiresAt', async () => { + const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code + expect( + query({ query: isValidInviteCodeQuery, variables: { code: unExpiringInviteCode } }), + ).resolves.toBeTruthy() + }) + + it('validates an invite code with expiresAt in the future', async () => { + const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null)[0].code + expect( + query({ query: isValidInviteCodeQuery, variables: { code: expiringInviteCode } }), + ).resolves.toBeTruthy() + }) + + it.skip('does not validate an invite code which expired in the past', async () => { + const lastWeek = new Date() + lastWeek.setDate(lastWeek.getDate() - 7) + const code = await Factory.build('inviteCode', { + expiresAt: lastWeek.toISOString(), + }) + expect( + query({ query: isValidInviteCodeQuery, variables: { code: code.code } }), + ).resolves.toBeFalsy() + }) }) }) diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql index 00ca99b75..e75764c1c 100644 --- a/backend/src/schema/types/type/InviteCode.gql +++ b/backend/src/schema/types/type/InviteCode.gql @@ -12,5 +12,6 @@ type Mutation { } type Query { - MyInviteCodes: [InviteCode] + MyInviteCodes: [InviteCode] + isValidInviteCode(code: ID): Boolean } From c6ff0723ee81f13c5cea9d98c07efaa9ecfbf1f6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 14 Jan 2021 22:34:53 +0100 Subject: [PATCH 06/16] inviteCodes: tests are working as expected, fixed factory --- backend/src/db/factories.js | 6 ++-- .../src/schema/resolvers/inviteCodes.spec.js | 33 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 8f1d3c718..0f2974d98 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -206,7 +206,7 @@ const emailDefaults = { } Factory.define('emailAddress') - .attr(emailDefaults) + .attrs(emailDefaults) .after((buildObject, options) => { return neode.create('EmailAddress', buildObject) }) @@ -218,13 +218,13 @@ Factory.define('unverifiedEmailAddress') }) const inviteCodeDefaults = { - code: generateInvieCode(), + code: () => generateInvieCode(), createdAt: () => new Date().toISOString(), expiresAt: () => null, } Factory.define('inviteCode') - .attr(inviteCodeDefaults) + .attrs(inviteCodeDefaults) .option('generatedById', null) .option('generatedBy', ['generatedById'], (generatedById) => { if (generatedById) return neode.find('User', generatedById) diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js index 9fa3e188b..f3df6ebba 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.js +++ b/backend/src/schema/resolvers/inviteCodes.spec.js @@ -147,7 +147,7 @@ describe('inviteCodes', () => { expect(inviteCodes).toHaveLength(2) }) - it('does not returns the created invite codes of other users when queried', async () => { + it('does not return the created invite codes of other users when queried', async () => { await Factory.build('inviteCode') const response = await query({ query: myInviteCodesQuery }) inviteCodes = response.data.MyInviteCodes @@ -156,27 +156,36 @@ describe('inviteCodes', () => { it('validates an invite code without expiresAt', async () => { const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code - expect( - query({ query: isValidInviteCodeQuery, variables: { code: unExpiringInviteCode } }), - ).resolves.toBeTruthy() + const result = await query({ + query: isValidInviteCodeQuery, + variables: { code: unExpiringInviteCode }, + }) + expect(result.data.isValidInviteCode).toBeTruthy() }) it('validates an invite code with expiresAt in the future', async () => { const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null)[0].code - expect( - query({ query: isValidInviteCodeQuery, variables: { code: expiringInviteCode } }), - ).resolves.toBeTruthy() + const result = await query({ + query: isValidInviteCodeQuery, + variables: { code: expiringInviteCode }, + }) + expect(result.data.isValidInviteCode).toBeTruthy() }) - it.skip('does not validate an invite code which expired in the past', async () => { + it('does not validate an invite code which expired in the past', async () => { const lastWeek = new Date() lastWeek.setDate(lastWeek.getDate() - 7) - const code = await Factory.build('inviteCode', { + const inviteCode = await Factory.build('inviteCode', { expiresAt: lastWeek.toISOString(), }) - expect( - query({ query: isValidInviteCodeQuery, variables: { code: code.code } }), - ).resolves.toBeFalsy() + const code = inviteCode.get('code') + const result = await query({ query: isValidInviteCodeQuery, variables: { code } }) + expect(result.data.isValidInviteCode).toBeFalsy() + }) + + it('does not validate an invite code which does not exits', async () => { + const result = await query({ query: isValidInviteCodeQuery, variables: { code: 'AAA' } }) + expect(result.data.isValidInviteCode).toBeFalsy() }) }) }) From cbf50014ed150a4b502c2130d07e975656f1ae06 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 15 Jan 2021 00:36:55 +0100 Subject: [PATCH 07/16] removed categories from posts --- .../validation/validationMiddleware.js | 2 -- .../validation/validationMiddleware.spec.js | 8 ++--- backend/src/schema/resolvers/posts.js | 7 +--- backend/src/schema/types/type/Post.gql | 2 +- backend/src/schema/types/type/User.gql | 2 +- .../ContributionForm/ContributionForm.spec.js | 34 ++++++++++--------- .../ContributionForm/ContributionForm.vue | 23 ++----------- webapp/pages/post/_id/_slug/index.vue | 17 ---------- 8 files changed, 27 insertions(+), 68 deletions(-) diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index d36e64846..086b657ff 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -138,8 +138,6 @@ export default { Mutation: { CreateComment: validateCreateComment, UpdateComment: validateUpdateComment, - CreatePost: validatePost, - UpdatePost: validateUpdatePost, UpdateUser: validateUpdateUser, fileReport: validateReport, review: validateReview, diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js index 74a343eeb..efb85bd10 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.js +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -240,7 +240,7 @@ describe('validateCreateComment', () => { describe('categories', () => { describe('null', () => { - it('throws UserInputError', async () => { + it.skip('throws UserInputError', async () => { createPostVariables = { ...createPostVariables, categoryIds: null } await expect( mutate({ mutation: createPostMutation, variables: createPostVariables }), @@ -256,7 +256,7 @@ describe('validateCreateComment', () => { }) describe('empty', () => { - it('throws UserInputError', async () => { + it.skip('throws UserInputError', async () => { createPostVariables = { ...createPostVariables, categoryIds: [] } await expect( mutate({ mutation: createPostMutation, variables: createPostVariables }), @@ -272,7 +272,7 @@ describe('validateCreateComment', () => { }) describe('more than 3 categoryIds', () => { - it('throws UserInputError', async () => { + it.skip('throws UserInputError', async () => { createPostVariables = { ...createPostVariables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'], @@ -313,7 +313,7 @@ describe('validateCreateComment', () => { } }) - it('requires at least one category for successful update', async () => { + it.skip('requires at least one category for successful update', async () => { await expect( mutate({ mutation: updatePostMutation, variables: updatePostVariables }), ).resolves.toMatchObject({ diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index f209158fe..14e645730 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -76,7 +76,6 @@ export default { }, Mutation: { CreatePost: async (_parent, params, context, _resolveInfo) => { - const { categoryIds } = params const { image: imageInput } = params delete params.categoryIds delete params.image @@ -92,13 +91,9 @@ export default { WITH post MATCH (author:User {id: $userId}) MERGE (post)<-[:WROTE]-(author) - WITH post - UNWIND $categoryIds AS categoryId - MATCH (category:Category {id: categoryId}) - MERGE (post)-[:CATEGORIZED]->(category) RETURN post {.*} `, - { userId: context.user.id, categoryIds, params }, + { userId: context.user.id, params }, ) const [post] = createPostTransactionResponse.records.map((record) => record.get('post')) if (imageInput) { diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index dc6a00a41..37f9dd176 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -136,7 +136,7 @@ type Post { """ ) tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") + categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") commentsCount: Int! diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index e6e7191c5..08682494f 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -104,7 +104,7 @@ type User { shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") + categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index be2228845..cabf77455 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -17,6 +17,7 @@ config.stubs['client-only'] = '' config.stubs['nuxt-link'] = '' config.stubs['v-popover'] = '' +/* const categories = [ { id: 'cat3', @@ -44,6 +45,7 @@ const categories = [ icon: 'tree', }, ] +*/ describe('ContributionForm.vue', () => { let wrapper, @@ -136,7 +138,7 @@ describe('ContributionForm.vue', () => { describe('CreatePost', () => { describe('invalid form submission', () => { beforeEach(async () => { - wrapper.find(CategoriesSelect).setData({ categories }) + // wrapper.find(CategoriesSelect).setData({ categories }) postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) @@ -144,10 +146,10 @@ describe('ContributionForm.vue', () => { .findAll('li') .filter((language) => language.text() === 'English') englishLanguage.trigger('click') - dataPrivacyButton = await wrapper + /* dataPrivacyButton = await wrapper .find(CategoriesSelect) - .find('[data-test="category-buttons-cat12"]') - dataPrivacyButton.trigger('click') + .find('[data-test="category-buttons-cat12"]') + dataPrivacyButton.trigger('click') */ }) it('title cannot be empty', async () => { @@ -174,7 +176,7 @@ describe('ContributionForm.vue', () => { expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) - it('has at least one category', async () => { + it.skip('has at least one category', async () => { dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') @@ -183,7 +185,7 @@ describe('ContributionForm.vue', () => { expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) - it('has no more than three categories', async () => { + it.skip('has no more than three categories', async () => { wrapper.vm.formData.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27'] await Vue.nextTick() wrapper.find('form').trigger('submit') @@ -200,23 +202,23 @@ describe('ContributionForm.vue', () => { content: postContent, language: 'en', id: null, - categoryIds: ['cat12'], + // categoryIds: ['cat12'], image: null, }, } postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) - wrapper.find(CategoriesSelect).setData({ categories }) + // wrapper.find(CategoriesSelect).setData({ categories }) englishLanguage = wrapper .findAll('li') .filter((language) => language.text() === 'English') englishLanguage.trigger('click') await Vue.nextTick() - dataPrivacyButton = await wrapper + /* dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') - dataPrivacyButton.trigger('click') + dataPrivacyButton.trigger('click') */ await Vue.nextTick() }) @@ -293,16 +295,16 @@ describe('ContributionForm.vue', () => { postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) categoryIds = ['cat12'] - wrapper.find(CategoriesSelect).setData({ categories }) + // wrapper.find(CategoriesSelect).setData({ categories }) englishLanguage = wrapper .findAll('li') .filter((language) => language.text() === 'English') englishLanguage.trigger('click') await Vue.nextTick() - dataPrivacyButton = await wrapper + /* dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') - dataPrivacyButton.trigger('click') + dataPrivacyButton.trigger('click') */ await Vue.nextTick() }) @@ -365,7 +367,7 @@ describe('ContributionForm.vue', () => { content: propsData.contribution.content, language: propsData.contribution.language, id: propsData.contribution.id, - categoryIds: ['cat12'], + // categoryIds: ['cat12'], image: { sensitive: false, }, @@ -380,9 +382,9 @@ describe('ContributionForm.vue', () => { expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) - it('supports updating categories', async () => { + it.skip('supports updating categories', async () => { expectedParams.variables.categoryIds.push('cat3') - wrapper.find(CategoriesSelect).setData({ categories }) + // wrapper.find(CategoriesSelect).setData({ categories }) await Vue.nextTick() const healthWellbeingButton = await wrapper .find(CategoriesSelect) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 0cbd90563..8db89173f 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -50,11 +50,6 @@ {{ contentLength }} - - - {{ formData.categoryIds.length }} / 3 - - { return { label: locale.name, value: locale.code } @@ -119,21 +112,10 @@ export default { imageAspectRatio, imageBlurred, language: languageOptions.find((option) => option.value === language) || null, - categoryIds: categories ? categories.map((category) => category.id) : [], }, formSchema: { title: { required: true, min: 3, max: 100 }, content: { required: true }, - categoryIds: { - type: 'array', - required: true, - validator: (_, value = []) => { - if (value.length === 0 || value.length > 3) { - return [new Error(this.$t('common.validations.categories'))] - } - return [] - }, - }, language: { required: true }, imageBlurred: { required: false }, }, @@ -155,7 +137,7 @@ export default { methods: { submit() { let image = null - const { title, content, categoryIds } = this.formData + const { title, content } = this.formData if (this.formData.image) { image = { sensitive: this.formData.imageBlurred, @@ -172,7 +154,6 @@ export default { variables: { title, content, - categoryIds, id: this.contribution.id || null, language: this.formData.language.value, image, diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index c0388da87..a2147db41 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -46,21 +46,6 @@ - -
- - - - - - {{ post.language.toUpperCase() }} - -
@@ -110,7 +95,6 @@