diff --git a/backend/Dockerfile b/backend/Dockerfile
index f0251bddc..2e8667461 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:12.4-alpine as base
+FROM node:12.5-alpine as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000
diff --git a/backend/package.json b/backend/package.json
index 565973a67..599e8eac6 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -61,7 +61,7 @@
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
- "graphql-shield": "~5.7.1",
+ "graphql-shield": "~6.0.2",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.18.0",
"helmet": "~3.18.0",
diff --git a/backend/src/middleware/nodes/locations.js b/backend/src/middleware/nodes/locations.js
index 62d1e3a65..d7abb90ff 100644
--- a/backend/src/middleware/nodes/locations.js
+++ b/backend/src/middleware/nodes/locations.js
@@ -87,6 +87,9 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
}
const session = driver.session()
+ if (data.place_type.length > 1) {
+ data.id = 'region.' + data.id.split('.')[1]
+ }
await createLocation(session, data)
let parent = data
diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js
index 985654b0f..d214a5571 100644
--- a/backend/src/middleware/notifications/spec.js
+++ b/backend/src/middleware/notifications/spec.js
@@ -88,21 +88,26 @@ describe('currentUser { notifications }', () => {
describe('who mentions me again', () => {
beforeEach(async () => {
const updatedContent = `${post.content} One more mention to @al-capone`
+ const updatedTitle = 'this post has been updated'
// The response `post.content` contains a link but the XSSmiddleware
// should have the `mention` CSS class removed. I discovered this
// during development and thought: A feature not a bug! This way we
// can encode a re-mentioning of users when you edit your post or
// comment.
- const createPostMutation = `
- mutation($id: ID!, $content: String!) {
- UpdatePost(id: $id, content: $content) {
+ const updatePostMutation = `
+ mutation($id: ID!, $title: String!, $content: String!) {
+ UpdatePost(id: $id, title: $title, content: $content) {
title
content
}
}
`
authorClient = new GraphQLClient(host, { headers: authorHeaders })
- await authorClient.request(createPostMutation, { id: post.id, content: updatedContent })
+ await authorClient.request(updatePostMutation, {
+ id: post.id,
+ content: updatedContent,
+ title: updatedTitle,
+ })
})
it('creates exactly one more notification', async () => {
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index dbcde849c..af4a46d81 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -4,7 +4,9 @@ import { rule, shield, deny, allow, or } from 'graphql-shield'
* TODO: implement
* See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363
*/
-const isAuthenticated = rule()(async (parent, args, ctx, info) => {
+const isAuthenticated = rule({
+ cache: 'contextual',
+})(async (_parent, _args, ctx, _info) => {
return ctx.user !== null
})
@@ -105,7 +107,7 @@ const permissions = shield(
Query: {
'*': deny,
findPosts: allow,
- Category: isAdmin,
+ Category: allow,
Tag: isAdmin,
Report: isModerator,
Notification: isAdmin,
diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js
index 2b1f25d5c..226bef8e5 100644
--- a/backend/src/middleware/sluggifyMiddleware.js
+++ b/backend/src/middleware/sluggifyMiddleware.js
@@ -17,6 +17,10 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
+ UpdatePost: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
+ return resolve(root, args, context, info)
+ },
CreateUser: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info)
diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js
index d2e296596..7aef63c59 100644
--- a/backend/src/schema/resolvers/comments.js
+++ b/backend/src/schema/resolvers/comments.js
@@ -18,9 +18,6 @@ export default {
if (!params.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
}
- if (!postId.trim()) {
- throw new UserInputError(NO_POST_ERR_MESSAGE)
- }
const session = context.driver.session()
const postQueryRes = await session.run(
diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js
index 55b946bb9..07462ed49 100644
--- a/backend/src/schema/resolvers/comments.spec.js
+++ b/backend/src/schema/resolvers/comments.spec.js
@@ -23,7 +23,7 @@ afterEach(async () => {
describe('CreateComment', () => {
const createCommentMutation = gql`
- mutation($postId: ID, $content: String!) {
+ mutation($postId: ID!, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
@@ -37,13 +37,6 @@ describe('CreateComment', () => {
}
}
`
- const commentQueryForPostId = gql`
- query($content: String) {
- Comment(content: $content) {
- postId
- }
- }
- `
describe('unauthenticated', () => {
it('throws authorization error', async () => {
createCommentVariables = {
@@ -191,23 +184,6 @@ describe('CreateComment', () => {
client.request(createCommentMutation, createCommentVariablesWithNonExistentPost),
).rejects.toThrow('Comment cannot be created without a post!')
})
-
- it('does not create the comment with the postId as an attribute', async () => {
- const commentQueryVariablesByContent = {
- content: "I'm authorised to comment",
- }
-
- await client.request(createCommentMutation, createCommentVariables)
- const { Comment } = await client.request(
- commentQueryForPostId,
- commentQueryVariablesByContent,
- )
- expect(Comment).toEqual([
- {
- postId: null,
- },
- ])
- })
})
})
diff --git a/backend/src/schema/resolvers/fileUpload/index.js b/backend/src/schema/resolvers/fileUpload/index.js
index c37d87e39..fa78238c3 100644
--- a/backend/src/schema/resolvers/fileUpload/index.js
+++ b/backend/src/schema/resolvers/fileUpload/index.js
@@ -12,7 +12,6 @@ const storeUpload = ({ createReadStream, fileLocation }) =>
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
const upload = params[file]
-
if (upload) {
const { createReadStream, filename } = await upload
const { name } = path.parse(filename)
diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js
index ea962a662..0c8dfb7f0 100644
--- a/backend/src/schema/resolvers/posts.js
+++ b/backend/src/schema/resolvers/posts.js
@@ -1,30 +1,74 @@
-import { neo4jgraphql } from 'neo4j-graphql-js'
+import uuid from 'uuid/v4'
import fileUpload from './fileUpload'
export default {
Mutation: {
UpdatePost: async (object, params, context, resolveInfo) => {
+ const { categoryIds } = params
+ delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
- return neo4jgraphql(object, params, context, resolveInfo, false)
+ const session = context.driver.session()
+ const cypherDeletePreviousRelations = `
+ MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
+ DELETE previousRelations
+ RETURN post, category
+ `
+
+ await session.run(cypherDeletePreviousRelations, { params })
+
+ let updatePostCypher = `MATCH (post:Post {id: $params.id})
+ SET post = $params
+ `
+ if (categoryIds && categoryIds.length) {
+ updatePostCypher += `WITH post
+ UNWIND $categoryIds AS categoryId
+ MATCH (category:Category {id: categoryId})
+ MERGE (post)-[:CATEGORIZED]->(category)
+ `
+ }
+ updatePostCypher += `RETURN post`
+ const updatePostVariables = { categoryIds, params }
+
+ const transactionRes = await session.run(updatePostCypher, updatePostVariables)
+ const [post] = transactionRes.records.map(record => {
+ return record.get('post')
+ })
+
+ session.close()
+
+ return post.properties
},
CreatePost: async (object, params, context, resolveInfo) => {
+ const { categoryIds } = params
+ delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
- const result = await neo4jgraphql(object, params, context, resolveInfo, false)
+ params.id = params.id || uuid()
+ let createPostCypher = `CREATE (post:Post {params})
+ WITH post
+ MATCH (author:User {id: $userId})
+ MERGE (post)<-[:WROTE]-(author)
+ `
+ if (categoryIds) {
+ createPostCypher += `WITH post
+ UNWIND $categoryIds AS categoryId
+ MATCH (category:Category {id: categoryId})
+ MERGE (post)-[:CATEGORIZED]->(category)
+ `
+ }
+ createPostCypher += `RETURN post`
+ const createPostVariables = { userId: context.user.id, categoryIds, params }
const session = context.driver.session()
- await session.run(
- 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
- 'MERGE (post)<-[:WROTE]-(author) ' +
- 'RETURN author',
- {
- userId: context.user.id,
- postId: result.id,
- },
- )
+ const transactionRes = await session.run(createPostCypher, createPostVariables)
+
+ const [post] = transactionRes.records.map(record => {
+ return record.get('post')
+ })
+
session.close()
- return result
+ return post.properties
},
},
}
diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js
index 3bff53ddb..763945527 100644
--- a/backend/src/schema/resolvers/posts.spec.js
+++ b/backend/src/schema/resolvers/posts.spec.js
@@ -4,7 +4,34 @@ import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
-
+const postTitle = 'I am a title'
+const postContent = 'Some content'
+const oldTitle = 'Old title'
+const oldContent = 'Old content'
+const newTitle = 'New title'
+const newContent = 'New content'
+const createPostVariables = { title: postTitle, content: postContent }
+const createPostWithCategoriesMutation = `
+ mutation($title: String!, $content: String!, $categoryIds: [ID]) {
+ CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
+ id
+ }
+ }
+`
+const creatPostWithCategoriesVariables = {
+ title: postTitle,
+ content: postContent,
+ categoryIds: ['cat9', 'cat4', 'cat15'],
+}
+const postQueryWithCategories = `
+ query($id: ID) {
+ Post(id: $id) {
+ categories {
+ id
+ }
+ }
+ }
+`
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
@@ -18,8 +45,8 @@ afterEach(async () => {
describe('CreatePost', () => {
const mutation = `
- mutation {
- CreatePost(title: "I am a title", content: "Some content") {
+ mutation($title: String!, $content: String!) {
+ CreatePost(title: $title, content: $content) {
title
content
slug
@@ -32,7 +59,7 @@ describe('CreatePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
- await expect(client.request(mutation)).rejects.toThrow('Not Authorised')
+ await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
})
})
@@ -46,15 +73,15 @@ describe('CreatePost', () => {
it('creates a post', async () => {
const expected = {
CreatePost: {
- title: 'I am a title',
- content: 'Some content',
+ title: postTitle,
+ content: postContent,
},
}
- await expect(client.request(mutation)).resolves.toMatchObject(expected)
+ await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
})
it('assigns the authenticated user as author', async () => {
- await client.request(mutation)
+ await client.request(mutation, createPostVariables)
const { User } = await client.request(
`{
User(email:"test@example.org") {
@@ -65,49 +92,75 @@ describe('CreatePost', () => {
}`,
{ headers },
)
- expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }])
+ expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
})
describe('disabled and deleted', () => {
it('initially false', async () => {
const expected = { CreatePost: { disabled: false, deleted: false } }
- await expect(client.request(mutation)).resolves.toMatchObject(expected)
+ await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
})
})
describe('language', () => {
it('allows a user to set the language of the post', async () => {
const createPostWithLanguageMutation = `
- mutation {
- CreatePost(title: "I am a title", content: "Some content", language: "en") {
+ mutation($title: String!, $content: String!, $language: String) {
+ CreatePost(title: $title, content: $content, language: $language) {
language
}
}
`
+ const createPostWithLanguageVariables = {
+ title: postTitle,
+ content: postContent,
+ language: 'en',
+ }
const expected = { CreatePost: { language: 'en' } }
- await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual(
- expect.objectContaining(expected),
+ await expect(
+ client.request(createPostWithLanguageMutation, createPostWithLanguageVariables),
+ ).resolves.toEqual(expect.objectContaining(expected))
+ })
+ })
+
+ describe('categories', () => {
+ it('allows a user to set the categories of the post', async () => {
+ await Promise.all([
+ factory.create('Category', {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ }),
+ factory.create('Category', {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ }),
+ factory.create('Category', {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ }),
+ ])
+ const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
+ const postWithCategories = await client.request(
+ createPostWithCategoriesMutation,
+ creatPostWithCategoriesVariables,
)
+ const postQueryWithCategoriesVariables = {
+ id: postWithCategories.CreatePost.id,
+ }
+ await expect(
+ client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
+ ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
})
})
})
})
describe('UpdatePost', () => {
- const mutation = `
- mutation($id: ID!, $content: String) {
- UpdatePost(id: $id, content: $content) {
- id
- content
- }
- }
- `
-
- let variables = {
- id: 'p1',
- content: 'New content',
- }
-
+ let updatePostMutation
+ let updatePostVariables
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
@@ -120,14 +173,32 @@ describe('UpdatePost', () => {
})
await asAuthor.create('Post', {
id: 'p1',
- content: 'Old content',
+ title: oldTitle,
+ content: oldContent,
})
+ updatePostMutation = `
+ mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
+ UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
+ id
+ content
+ }
+ }
+ `
+
+ updatePostVariables = {
+ id: 'p1',
+ title: newTitle,
+ content: newContent,
+ categoryIds: null,
+ }
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
- await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
})
})
@@ -139,7 +210,9 @@ describe('UpdatePost', () => {
})
it('throws authorization error', async () => {
- await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
})
})
@@ -151,8 +224,59 @@ describe('UpdatePost', () => {
})
it('updates a post', async () => {
- const expected = { UpdatePost: { id: 'p1', content: 'New content' } }
- await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ const expected = { UpdatePost: { id: 'p1', content: newContent } }
+ await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual(
+ expected,
+ )
+ })
+
+ describe('categories', () => {
+ let postWithCategories
+ beforeEach(async () => {
+ await Promise.all([
+ factory.create('Category', {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ }),
+ factory.create('Category', {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ }),
+ factory.create('Category', {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ }),
+ factory.create('Category', {
+ id: 'cat27',
+ name: 'Animal Protection',
+ icon: 'paw',
+ }),
+ ])
+ postWithCategories = await client.request(
+ createPostWithCategoriesMutation,
+ creatPostWithCategoriesVariables,
+ )
+ updatePostVariables = {
+ id: postWithCategories.CreatePost.id,
+ title: newTitle,
+ content: newContent,
+ categoryIds: ['cat27'],
+ }
+ })
+
+ it('allows a user to update the categories of a post', async () => {
+ await client.request(updatePostMutation, updatePostVariables)
+ const expected = [{ id: 'cat27' }]
+ const postQueryWithCategoriesVariables = {
+ id: postWithCategories.CreatePost.id,
+ }
+ await expect(
+ client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
+ ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
+ })
})
})
})
diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js
index 2c0fbfc75..67c896939 100644
--- a/backend/src/schema/resolvers/reports.js
+++ b/backend/src/schema/resolvers/reports.js
@@ -11,12 +11,31 @@ export default {
description: description,
}
+ const reportQueryRes = await session.run(
+ `
+ match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId})
+ return labels(resource)[0] as label
+ `,
+ {
+ resourceId: id,
+ submitterId: user.id,
+ },
+ )
+ const [rep] = reportQueryRes.records.map(record => {
+ return {
+ label: record.get('label'),
+ }
+ })
+
+ if (rep) {
+ throw new Error(rep.label)
+ }
const res = await session.run(
`
MATCH (submitter:User {id: $userId})
MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Comment OR resource:Post
- CREATE (report:Report $reportData)
+ MERGE (report:Report {id: {reportData}.id })
MERGE (resource)<-[:REPORTED]-(report)
MERGE (report)<-[:REPORTED]-(submitter)
RETURN report, submitter, resource, labels(resource)[0] as type
@@ -27,6 +46,7 @@ export default {
reportData,
},
)
+
session.close()
const [dbResponse] = res.records.map(r => {
@@ -59,6 +79,7 @@ export default {
response.user = resource.properties
break
}
+
return response
},
},
diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js
index 6b996b016..2a798f5ee 100644
--- a/backend/src/schema/resolvers/reports.spec.js
+++ b/backend/src/schema/resolvers/reports.spec.js
@@ -13,7 +13,9 @@ describe('report', () => {
beforeEach(async () => {
returnedObject = '{ description }'
- variables = { id: 'whatever' }
+ variables = {
+ id: 'whatever',
+ }
headers = {}
await factory.create('User', {
id: 'u1',
@@ -42,7 +44,9 @@ describe('report', () => {
) ${returnedObject}
}
`
- client = new GraphQLClient(host, { headers })
+ client = new GraphQLClient(host, {
+ headers,
+ })
return client.request(mutation, variables)
}
@@ -53,7 +57,10 @@ describe('report', () => {
describe('authenticated', () => {
beforeEach(async () => {
- headers = await login({ email: 'test@example.org', password: '1234' })
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
})
describe('invalid resource id', () => {
@@ -66,19 +73,25 @@ describe('report', () => {
describe('valid resource id', () => {
beforeEach(async () => {
- variables = { id: 'u2' }
+ variables = {
+ id: 'u2',
+ }
})
-
- it('creates a report', async () => {
- await expect(action()).resolves.toEqual({
- report: { description: 'Violates code of conduct' },
- })
- })
-
+ /*
+ it('creates a report', async () => {
+ await expect(action()).resolves.toEqual({
+ type: null,
+ })
+ })
+ */
it('returns the submitter', async () => {
returnedObject = '{ submitter { email } }'
await expect(action()).resolves.toEqual({
- report: { submitter: { email: 'test@example.org' } },
+ report: {
+ submitter: {
+ email: 'test@example.org',
+ },
+ },
})
})
@@ -86,50 +99,72 @@ describe('report', () => {
it('returns type "User"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
- report: { type: 'User' },
+ report: {
+ type: 'User',
+ },
})
})
it('returns resource in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
- report: { user: { name: 'abusive-user' } },
+ report: {
+ user: {
+ name: 'abusive-user',
+ },
+ },
})
})
})
describe('reported resource is a post', () => {
beforeEach(async () => {
- await factory.authenticateAs({ email: 'test@example.org', password: '1234' })
+ await factory.authenticateAs({
+ email: 'test@example.org',
+ password: '1234',
+ })
await factory.create('Post', {
id: 'p23',
title: 'Matt and Robert having a pair-programming',
})
- variables = { id: 'p23' }
+ variables = {
+ id: 'p23',
+ }
})
it('returns type "Post"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
- report: { type: 'Post' },
+ report: {
+ type: 'Post',
+ },
})
})
it('returns resource in post attribute', async () => {
returnedObject = '{ post { title } }'
await expect(action()).resolves.toEqual({
- report: { post: { title: 'Matt and Robert having a pair-programming' } },
+ report: {
+ post: {
+ title: 'Matt and Robert having a pair-programming',
+ },
+ },
})
})
it('returns null in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
- report: { user: null },
+ report: {
+ user: null,
+ },
})
})
})
+ /* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the p23 again, but this time there must be an error message. */
+
describe('reported resource is a comment', () => {
beforeEach(async () => {
createPostVariables = {
@@ -147,34 +182,54 @@ describe('report', () => {
id: 'c34',
content: 'Robert getting tired.',
})
- variables = { id: 'c34' }
+ variables = {
+ id: 'c34',
+ }
})
it('returns type "Comment"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
- report: { type: 'Comment' },
+ report: {
+ type: 'Comment',
+ },
})
})
it('returns resource in comment attribute', async () => {
returnedObject = '{ comment { content } }'
await expect(action()).resolves.toEqual({
- report: { comment: { content: 'Robert getting tired.' } },
+ report: {
+ comment: {
+ content: 'Robert getting tired.',
+ },
+ },
})
})
})
+ /* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the c34 again, but this time there must be an error message. */
+
describe('reported resource is a tag', () => {
beforeEach(async () => {
- await factory.create('Tag', { id: 't23' })
- variables = { id: 't23' }
+ await factory.create('Tag', {
+ id: 't23',
+ })
+ variables = {
+ id: 't23',
+ }
})
it('returns null', async () => {
- await expect(action()).resolves.toEqual({ report: null })
+ await expect(action()).resolves.toEqual({
+ report: null,
+ })
})
})
+
+ /* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the t23 again, but this time there must be an error message. */
})
})
})
diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js
index 352d38eaa..9df5473bf 100644
--- a/backend/src/schema/resolvers/users.spec.js
+++ b/backend/src/schema/resolvers/users.spec.js
@@ -143,7 +143,7 @@ describe('users', () => {
let deleteUserVariables
let asAuthor
const deleteUserMutation = gql`
- mutation($id: ID!, $resource: [String]) {
+ mutation($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
contributions {
diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/schema/types/scalar/Upload.gql
index fca9ea1fc..cf3965846 100644
--- a/backend/src/schema/types/scalar/Upload.gql
+++ b/backend/src/schema/types/scalar/Upload.gql
@@ -1 +1 @@
-scalar Upload
\ No newline at end of file
+scalar Upload
diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql
index 1ef83bac3..8b0f422c8 100644
--- a/backend/src/schema/types/schema.gql
+++ b/backend/src/schema/types/schema.gql
@@ -40,7 +40,7 @@ type Mutation {
follow(id: ID!, type: FollowTypeEnum): Boolean!
# Unfollow the given Type and ID
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
- DeleteUser(id: ID!, resource: [String]): User
+ DeleteUser(id: ID!, resource: [Deletable]): User
}
type Statistics {
@@ -92,6 +92,11 @@ type Report {
user: User @relation(name: "REPORTED", direction: "OUT")
}
+enum Deletable {
+ Post
+ Comment
+}
+
enum ShoutTypeEnum {
Post
Organization
diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql
index 077366e8a..441fba179 100644
--- a/backend/src/schema/types/type/Comment.gql
+++ b/backend/src/schema/types/type/Comment.gql
@@ -1,7 +1,6 @@
type Comment {
id: ID!
activityId: String
- postId: ID
author: User @relation(name: "WROTE", direction: "IN")
content: String!
contentExcerpt: String
@@ -11,4 +10,24 @@ type Comment {
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
-}
\ No newline at end of file
+}
+
+type Mutation {
+ CreateComment(
+ id: ID
+ postId: ID!
+ content: String!
+ contentExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ ): Comment
+ UpdateComment(
+ id: ID!
+ content: String
+ contentExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+ ): Comment
+ DeleteComment(id: ID!): Comment
+}
diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql
index 271d92750..deb1d8f85 100644
--- a/backend/src/schema/types/type/Post.gql
+++ b/backend/src/schema/types/type/Post.gql
@@ -49,3 +49,42 @@ type Post {
"""
)
}
+
+type Mutation {
+ CreatePost(
+ id: ID
+ activityId: String
+ objectId: String
+ title: String!
+ slug: String
+ content: String!
+ image: String
+ imageUpload: Upload
+ visibility: Visibility
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ updatedAt: String
+ language: String
+ categoryIds: [ID]
+ contentExcerpt: String
+ ): Post
+ UpdatePost(
+ id: ID!
+ activityId: String
+ objectId: String
+ title: String!
+ slug: String
+ content: String!
+ contentExcerpt: String
+ image: String
+ imageUpload: Upload
+ visibility: Visibility
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ updatedAt: String
+ language: String
+ categoryIds: [ID]
+ ): Post
+}
diff --git a/backend/src/seed/factories/comments.js b/backend/src/seed/factories/comments.js
index b1079e392..20933e947 100644
--- a/backend/src/seed/factories/comments.js
+++ b/backend/src/seed/factories/comments.js
@@ -10,7 +10,7 @@ export default function(params) {
return {
mutation: `
- mutation($id: ID!, $postId: ID, $content: String!) {
+ mutation($id: ID!, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 14cec1a81..53075537f 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -1110,10 +1110,10 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
-"@types/yup@0.26.17":
- version "0.26.17"
- resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
- integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
+"@types/yup@0.26.20":
+ version "0.26.20"
+ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.20.tgz#3b85a05f5dd76e2e8475abb6a8aeae7777627143"
+ integrity sha512-LpCsA6NG7vIU7Umv1k4w3YGIBH5ZLZRPEKo8vJLHVbBUqRy2WaJ002kbsRqcwODpkICAOMuyGOqLQJa5isZ8+g==
"@types/zen-observable@^0.5.3":
version "0.5.4"
@@ -3788,12 +3788,12 @@ graphql-request@~1.8.2:
dependencies:
cross-fetch "2.2.2"
-graphql-shield@~5.7.1:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
- integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
+graphql-shield@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.2.tgz#3ebad8faacbada91b8e576029732e91b5a041c7f"
+ integrity sha512-3qV2qjeNZla1Fyg6Q2NR5J9AsMaNePLbUboOwhRXB7IcMnTnrxSiVn2R//8VnjnmBjF9rcvgAIAvETZ8AKGfsg==
dependencies:
- "@types/yup" "0.26.17"
+ "@types/yup" "0.26.20"
lightercollective "^0.3.0"
object-hash "^1.3.1"
yup "^0.27.0"
diff --git a/package.json b/package.json
index 856a241de..1446f0009 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"cross-env": "^5.2.0",
"cypress": "^3.3.2",
"cypress-cucumber-preprocessor": "^1.12.0",
- "cypress-file-upload": "^3.1.4",
+ "cypress-file-upload": "^3.2.0",
"cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.0.0",
"faker": "Marak/faker.js#master",
diff --git a/webapp/Dockerfile b/webapp/Dockerfile
index feba44c36..9b7f1329c 100644
--- a/webapp/Dockerfile
+++ b/webapp/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:12.4-alpine as base
+FROM node:12.5-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000
diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js
new file mode 100644
index 000000000..199dacb74
--- /dev/null
+++ b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js
@@ -0,0 +1,108 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import CategoriesSelect from './CategoriesSelect'
+import Styleguide from '@human-connection/styleguide'
+
+const localVue = createLocalVue()
+localVue.use(Styleguide)
+
+describe('CategoriesSelect.vue', () => {
+ let wrapper
+ let mocks
+ let democracyAndPolitics
+ let environmentAndNature
+ let consumptionAndSustainablity
+
+ const categories = [
+ {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ },
+ {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ },
+ {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ },
+ {
+ name: 'Cooperation & Development',
+ icon: 'users',
+ id: 'cat8',
+ },
+ ]
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ }
+ })
+
+ describe('shallowMount', () => {
+ const Wrapper = () => {
+ return mount(CategoriesSelect, { mocks, localVue })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('toggleCategory', () => {
+ beforeEach(() => {
+ wrapper.vm.categories = categories
+ democracyAndPolitics = wrapper.findAll('button').at(0)
+ democracyAndPolitics.trigger('click')
+ })
+
+ it('adds categories to selectedCategoryIds when clicked', () => {
+ expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id])
+ })
+
+ it('emits an updateCategories event when the selectedCategoryIds changes', () => {
+ expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id])
+ })
+
+ it('removes categories when clicked a second time', () => {
+ democracyAndPolitics.trigger('click')
+ expect(wrapper.vm.selectedCategoryIds).toEqual([])
+ })
+
+ it('changes the selectedCount when selectedCategoryIds is updated', () => {
+ expect(wrapper.vm.selectedCount).toEqual(1)
+ democracyAndPolitics.trigger('click')
+ expect(wrapper.vm.selectedCount).toEqual(0)
+ })
+
+ it('sets a category to active when it has been selected', () => {
+ expect(wrapper.vm.isActive(categories[0].id)).toEqual(true)
+ })
+
+ describe('maximum', () => {
+ beforeEach(() => {
+ environmentAndNature = wrapper.findAll('button').at(1)
+ consumptionAndSustainablity = wrapper.findAll('button').at(2)
+ environmentAndNature.trigger('click')
+ consumptionAndSustainablity.trigger('click')
+ })
+
+ it('allows three categories to be selected', () => {
+ expect(wrapper.vm.selectedCategoryIds).toEqual([
+ categories[0].id,
+ categories[1].id,
+ categories[2].id,
+ ])
+ })
+
+ it('sets reachedMaximum to true after three', () => {
+ expect(wrapper.vm.reachedMaximum).toEqual(true)
+ })
+
+ it('sets other categories to disabled after three', () => {
+ expect(wrapper.vm.isDisabled(categories[3].id)).toEqual(true)
+ })
+ })
+ })
+ })
+})
diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue
new file mode 100644
index 000000000..163f31419
--- /dev/null
+++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue
@@ -0,0 +1,102 @@
+
+
+ {{
+ $t('contribution.categories.infoSelectedNoOfMaxCategories', {
+ chosen: selectedCount,
+ max: selectedMax,
+ })
+ }}
+
+