mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Implement 'CreateGroup' with tests
This commit is contained in:
parent
9632d0f852
commit
f565e5fb6a
@ -28,3 +28,5 @@ AWS_BUCKET=
|
||||
|
||||
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
|
||||
EMAIL_SUPPORT="devops@ocelot.social"
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
@ -86,6 +86,7 @@ const options = {
|
||||
ORGANIZATION_URL: emails.ORGANIZATION_LINK,
|
||||
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
|
||||
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
}
|
||||
|
||||
// Check if all required configs are present
|
||||
|
||||
29
backend/src/db/graphql/mutations.ts
Normal file
29
backend/src/db/graphql/mutations.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createGroupMutation = gql`
|
||||
mutation (
|
||||
$id: ID,
|
||||
$name: String!,
|
||||
$slug: String,
|
||||
$about: String,
|
||||
$categoryIds: [ID]
|
||||
) {
|
||||
CreateGroup(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
about: $about
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
about
|
||||
disabled
|
||||
deleted
|
||||
owner {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -59,7 +59,7 @@ class Store {
|
||||
const session = driver.session()
|
||||
await createDefaultAdminUser(session)
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
||||
return Promise.all(
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
|
||||
@ -2,6 +2,11 @@ import trunc from 'trunc-html'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
|
||||
@ -140,6 +140,7 @@ export default shield(
|
||||
Signup: or(publicRegistration, inviteRegistration, isAdmin),
|
||||
SignupVerification: allow,
|
||||
UpdateUser: onlyYourself,
|
||||
CreateGroup: isAuthenticated,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
|
||||
@ -26,6 +26,10 @@ export default {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Group')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import slugify from 'slug'
|
||||
|
||||
export default async function uniqueSlug(string, isUnique) {
|
||||
const slug = slugify(string || 'anonymous', {
|
||||
lower: true,
|
||||
|
||||
@ -15,7 +15,8 @@ export default {
|
||||
wasSeeded: 'boolean', // Wolle: used or needed?
|
||||
locationName: { type: 'string', allow: [null] },
|
||||
about: { type: 'string', allow: [null, ''] }, // Wolle: null?
|
||||
description: { type: 'string', allow: [null, ''] }, // Wolle: null? HTML with Tiptap, similar to post content
|
||||
description: { type: 'string', allow: [null, ''] }, // Wolle: null? HTML with Tiptap, similar to post content, wie bei Posts "content: { type: 'string', disallow: [null], min: 3 },"?
|
||||
descriptionExcerpt: { type: 'string', allow: [null] },
|
||||
// Wolle: followedBy: {
|
||||
// type: 'relationship',
|
||||
// relationship: 'FOLLOWS',
|
||||
@ -25,8 +26,14 @@ export default {
|
||||
// createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
// },
|
||||
// },
|
||||
owner: {
|
||||
type: 'relationship',
|
||||
relationship: 'OWNS',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
// Wolle: correct this way?
|
||||
members: { type: 'relationship', relationship: 'MEMBERS', target: 'User', direction: 'out' },
|
||||
// members: { type: 'relationship', relationship: 'MEMBERS', target: 'User', direction: 'out' },
|
||||
// Wolle: needed? lastActiveAt: { type: 'string', isoDate: true },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
|
||||
224
backend/src/schema/resolvers/groups.js
Normal file
224
backend/src/schema/resolvers/groups.js
Normal file
@ -0,0 +1,224 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
// Wolle: import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
// Wolle: import { isEmpty } from 'lodash'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import CONFIG from '../../config'
|
||||
// Wolle: import { mergeImage, deleteImage } from './images/images'
|
||||
import Resolver from './helpers/Resolver'
|
||||
// Wolle: import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||
|
||||
// Wolle: const maintainPinnedPosts = (params) => {
|
||||
// const pinnedPostFilter = { pinned: true }
|
||||
// if (isEmpty(params.filter)) {
|
||||
// params.filter = { OR: [pinnedPostFilter, {}] }
|
||||
// } else {
|
||||
// params.filter = { OR: [pinnedPostFilter, { ...params.filter }] }
|
||||
// }
|
||||
// return params
|
||||
// }
|
||||
|
||||
export default {
|
||||
// Wolle: Query: {
|
||||
// Post: async (object, params, context, resolveInfo) => {
|
||||
// params = await filterForMutedUsers(params, context)
|
||||
// params = await maintainPinnedPosts(params)
|
||||
// return neo4jgraphql(object, params, context, resolveInfo)
|
||||
// },
|
||||
// findPosts: async (object, params, context, resolveInfo) => {
|
||||
// params = await filterForMutedUsers(params, context)
|
||||
// return neo4jgraphql(object, params, context, resolveInfo)
|
||||
// },
|
||||
// profilePagePosts: async (object, params, context, resolveInfo) => {
|
||||
// params = await filterForMutedUsers(params, context)
|
||||
// return neo4jgraphql(object, params, context, resolveInfo)
|
||||
// },
|
||||
// PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||
// const { postId, data } = params
|
||||
// const session = context.driver.session()
|
||||
// const readTxResultPromise = session.readTransaction(async (transaction) => {
|
||||
// const emotionsCountTransactionResponse = await transaction.run(
|
||||
// `
|
||||
// MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
// RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
// `,
|
||||
// { postId, data },
|
||||
// )
|
||||
// return emotionsCountTransactionResponse.records.map(
|
||||
// (record) => record.get('emotionsCount').low,
|
||||
// )
|
||||
// })
|
||||
// try {
|
||||
// const [emotionsCount] = await readTxResultPromise
|
||||
// return emotionsCount
|
||||
// } finally {
|
||||
// session.close()
|
||||
// }
|
||||
// },
|
||||
// PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||
// const { postId } = params
|
||||
// const session = context.driver.session()
|
||||
// const readTxResultPromise = session.readTransaction(async (transaction) => {
|
||||
// const emotionsTransactionResponse = await transaction.run(
|
||||
// `
|
||||
// MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
// RETURN collect(emoted.emotion) as emotion
|
||||
// `,
|
||||
// { userId: context.user.id, postId },
|
||||
// )
|
||||
// return emotionsTransactionResponse.records.map((record) => record.get('emotion'))
|
||||
// })
|
||||
// try {
|
||||
// const [emotions] = await readTxResultPromise
|
||||
// return emotions
|
||||
// } finally {
|
||||
// session.close()
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
Mutation: {
|
||||
CreateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params.id = params.id || uuid()
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const categoriesCypher =
|
||||
CONFIG.CATEGORIES_ACTIVE && categoryIds
|
||||
? `WITH group
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (group)-[:CATEGORIZED]->(category)`
|
||||
: ''
|
||||
const ownercreateGroupTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (group:Group)
|
||||
SET group += $params
|
||||
SET group.createdAt = toString(datetime())
|
||||
SET group.updatedAt = toString(datetime())
|
||||
WITH group
|
||||
MATCH (owner:User {id: $userId})
|
||||
MERGE (group)<-[:OWNS]-(owner)
|
||||
MERGE (group)<-[:ADMINISTERS]-(owner)
|
||||
${categoriesCypher}
|
||||
RETURN group {.*}
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
const [group] = ownercreateGroupTransactionResponse.records.map((record) =>
|
||||
record.get('group'),
|
||||
)
|
||||
return group
|
||||
})
|
||||
try {
|
||||
const group = await writeTxResultPromise
|
||||
return group
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Group with this slug already exists!')
|
||||
throw new Error(e)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
// UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
// const { categoryIds } = params
|
||||
// const { image: imageInput } = params
|
||||
// delete params.categoryIds
|
||||
// delete params.image
|
||||
// const session = context.driver.session()
|
||||
// let updatePostCypher = `
|
||||
// MATCH (post:Post {id: $params.id})
|
||||
// SET post += $params
|
||||
// SET post.updatedAt = toString(datetime())
|
||||
// WITH post
|
||||
// `
|
||||
|
||||
// if (categoryIds && categoryIds.length) {
|
||||
// const cypherDeletePreviousRelations = `
|
||||
// MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
// DELETE previousRelations
|
||||
// RETURN post, category
|
||||
// `
|
||||
|
||||
// await session.writeTransaction((transaction) => {
|
||||
// return transaction.run(cypherDeletePreviousRelations, { params })
|
||||
// })
|
||||
|
||||
// updatePostCypher += `
|
||||
// UNWIND $categoryIds AS categoryId
|
||||
// MATCH (category:Category {id: categoryId})
|
||||
// MERGE (post)-[:CATEGORIZED]->(category)
|
||||
// WITH post
|
||||
// `
|
||||
// }
|
||||
|
||||
// updatePostCypher += `RETURN post {.*}`
|
||||
// const updatePostVariables = { categoryIds, params }
|
||||
// try {
|
||||
// const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
// const updatePostTransactionResponse = await transaction.run(
|
||||
// updatePostCypher,
|
||||
// updatePostVariables,
|
||||
// )
|
||||
// const [post] = updatePostTransactionResponse.records.map((record) => record.get('post'))
|
||||
// await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
|
||||
// return post
|
||||
// })
|
||||
// const post = await writeTxResultPromise
|
||||
// return post
|
||||
// } finally {
|
||||
// session.close()
|
||||
// }
|
||||
// },
|
||||
|
||||
// DeletePost: async (object, args, context, resolveInfo) => {
|
||||
// const session = context.driver.session()
|
||||
// const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
// const deletePostTransactionResponse = await transaction.run(
|
||||
// `
|
||||
// MATCH (post:Post {id: $postId})
|
||||
// OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
// SET post.deleted = TRUE
|
||||
// SET post.content = 'UNAVAILABLE'
|
||||
// SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
// SET post.title = 'UNAVAILABLE'
|
||||
// SET comment.deleted = TRUE
|
||||
// RETURN post {.*}
|
||||
// `,
|
||||
// { postId: args.id },
|
||||
// )
|
||||
// const [post] = deletePostTransactionResponse.records.map((record) => record.get('post'))
|
||||
// await deleteImage(post, 'HERO_IMAGE', { transaction })
|
||||
// return post
|
||||
// })
|
||||
// try {
|
||||
// const post = await writeTxResultPromise
|
||||
// return post
|
||||
// } finally {
|
||||
// session.close()
|
||||
// }
|
||||
},
|
||||
Group: {
|
||||
...Resolver('Group', {
|
||||
// Wolle: undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
|
||||
hasMany: {
|
||||
// Wolle: tags: '-[:TAGGED]->(related:Tag)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
},
|
||||
hasOne: {
|
||||
owner: '<-[:OWNS]-(related:User)',
|
||||
// Wolle: image: '-[:HERO_IMAGE]->(related:Image)',
|
||||
},
|
||||
// Wolle: count: {
|
||||
// contributionsCount:
|
||||
// '-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||
// },
|
||||
// Wolle: boolean: {
|
||||
// shoutedByCurrentUser:
|
||||
// 'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
||||
// viewedTeaserByCurrentUser:
|
||||
// 'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
// },
|
||||
}),
|
||||
},
|
||||
}
|
||||
610
backend/src/schema/resolvers/groups.spec.js
Normal file
610
backend/src/schema/resolvers/groups.spec.js
Normal file
@ -0,0 +1,610 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { createGroupMutation } from '../../db/graphql/mutations'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
// Wolle: let query
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let user
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
let variables = {}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
// Wolle: query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
user = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'current-user',
|
||||
name: 'TestUser',
|
||||
},
|
||||
{
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat27',
|
||||
name: 'Animal Protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
])
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
// 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('Group', () => {
|
||||
// describe('can be filtered', () => {
|
||||
// let followedUser, happyPost, cryPost
|
||||
// beforeEach(async () => {
|
||||
// ;[followedUser] = await Promise.all([
|
||||
// Factory.build(
|
||||
// 'user',
|
||||
// {
|
||||
// id: 'followed-by-me',
|
||||
// name: 'Followed User',
|
||||
// },
|
||||
// {
|
||||
// email: 'followed@example.org',
|
||||
// password: '1234',
|
||||
// },
|
||||
// ),
|
||||
// ])
|
||||
// ;[happyPost, cryPost] = await Promise.all([
|
||||
// Factory.build('post', { id: 'happy-post' }, { categoryIds: ['cat4'] }),
|
||||
// Factory.build('post', { id: 'cry-post' }, { categoryIds: ['cat15'] }),
|
||||
// Factory.build(
|
||||
// 'post',
|
||||
// {
|
||||
// id: 'post-by-followed-user',
|
||||
// },
|
||||
// {
|
||||
// categoryIds: ['cat9'],
|
||||
// author: followedUser,
|
||||
// },
|
||||
// ),
|
||||
// ])
|
||||
// })
|
||||
|
||||
// describe('no filter', () => {
|
||||
// it('returns all posts', async () => {
|
||||
// const postQueryNoFilters = gql`
|
||||
// query Post($filter: _PostFilter) {
|
||||
// Post(filter: $filter) {
|
||||
// id
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
// const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }]
|
||||
// variables = { filter: {} }
|
||||
// await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({
|
||||
// data: {
|
||||
// Post: expect.arrayContaining(expected),
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
// /* it('by categories', async () => {
|
||||
// const postQueryFilteredByCategories = gql`
|
||||
// query Post($filter: _PostFilter) {
|
||||
// Post(filter: $filter) {
|
||||
// id
|
||||
// categories {
|
||||
// id
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
// const expected = {
|
||||
// data: {
|
||||
// Post: [
|
||||
// {
|
||||
// id: 'post-by-followed-user',
|
||||
// categories: [{ id: 'cat9' }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }
|
||||
// variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } }
|
||||
// await expect(
|
||||
// query({ query: postQueryFilteredByCategories, variables }),
|
||||
// ).resolves.toMatchObject(expected)
|
||||
// }) */
|
||||
|
||||
// describe('by emotions', () => {
|
||||
// const postQueryFilteredByEmotions = gql`
|
||||
// query Post($filter: _PostFilter) {
|
||||
// Post(filter: $filter) {
|
||||
// id
|
||||
// emotions {
|
||||
// emotion
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
// it('filters by single emotion', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// Post: [
|
||||
// {
|
||||
// id: 'happy-post',
|
||||
// emotions: [{ emotion: 'happy' }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }
|
||||
// await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
|
||||
// variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } }
|
||||
// await expect(
|
||||
// query({ query: postQueryFilteredByEmotions, variables }),
|
||||
// ).resolves.toMatchObject(expected)
|
||||
// })
|
||||
|
||||
// it('filters by multiple emotions', async () => {
|
||||
// const expected = [
|
||||
// {
|
||||
// id: 'happy-post',
|
||||
// emotions: [{ emotion: 'happy' }],
|
||||
// },
|
||||
// {
|
||||
// id: 'cry-post',
|
||||
// emotions: [{ emotion: 'cry' }],
|
||||
// },
|
||||
// ]
|
||||
// await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
|
||||
// await user.relateTo(cryPost, 'emoted', { emotion: 'cry' })
|
||||
// variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } }
|
||||
// await expect(
|
||||
// query({ query: postQueryFilteredByEmotions, variables }),
|
||||
// ).resolves.toMatchObject({
|
||||
// data: {
|
||||
// Post: expect.arrayContaining(expected),
|
||||
// },
|
||||
// errors: undefined,
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
// it('by followed-by', async () => {
|
||||
// const postQueryFilteredByUsersFollowed = gql`
|
||||
// query Post($filter: _PostFilter) {
|
||||
// Post(filter: $filter) {
|
||||
// id
|
||||
// author {
|
||||
// id
|
||||
// name
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
// await user.relateTo(followedUser, 'following')
|
||||
// variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } }
|
||||
// await expect(
|
||||
// query({ query: postQueryFilteredByUsersFollowed, variables }),
|
||||
// ).resolves.toMatchObject({
|
||||
// data: {
|
||||
// Post: [
|
||||
// {
|
||||
// id: 'post-by-followed-user',
|
||||
// author: { id: 'followed-by-me', name: 'Followed User' },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// errors: undefined,
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('CreateGroup', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'g589',
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'We will change the world!',
|
||||
categoryIds,
|
||||
}
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: createGroupMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('creates a group', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
CreateGroup: {
|
||||
// Wolle: id: 'g589',
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'We will change the world!',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('assigns the authenticated user as owner', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
CreateGroup: {
|
||||
name: 'The Best Group',
|
||||
owner: {
|
||||
name: 'TestUser',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('`disabled` and `deleted` default to `false`', async () => {
|
||||
const expected = { data: { CreateGroup: { disabled: false, deleted: false } } }
|
||||
await expect(mutate({ mutation: createGroupMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// describe('UpdatePost', () => {
|
||||
// let author, newlyCreatedPost
|
||||
// const updatePostMutation = gql`
|
||||
// mutation ($id: ID!, $title: String!, $content: String!, $image: ImageInput) {
|
||||
// UpdatePost(id: $id, title: $title, content: $content, image: $image) {
|
||||
// id
|
||||
// title
|
||||
// content
|
||||
// author {
|
||||
// name
|
||||
// slug
|
||||
// }
|
||||
// createdAt
|
||||
// updatedAt
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
// beforeEach(async () => {
|
||||
// author = await Factory.build('user', { slug: 'the-author' })
|
||||
// newlyCreatedPost = await Factory.build(
|
||||
// 'post',
|
||||
// {
|
||||
// id: 'p9876',
|
||||
// title: 'Old title',
|
||||
// content: 'Old content',
|
||||
// },
|
||||
// {
|
||||
// author,
|
||||
// categoryIds,
|
||||
// },
|
||||
// )
|
||||
|
||||
// variables = {
|
||||
// id: 'p9876',
|
||||
// title: 'New title',
|
||||
// content: 'New content',
|
||||
// }
|
||||
// })
|
||||
|
||||
// describe('unauthenticated', () => {
|
||||
// it('throws authorization error', async () => {
|
||||
// authenticatedUser = null
|
||||
// expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
// errors: [{ message: 'Not Authorised!' }],
|
||||
// data: { UpdatePost: null },
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('authenticated but not the author', () => {
|
||||
// beforeEach(async () => {
|
||||
// authenticatedUser = await user.toJson()
|
||||
// })
|
||||
|
||||
// it('throws authorization error', async () => {
|
||||
// const { errors } = await mutate({ mutation: updatePostMutation, variables })
|
||||
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('authenticated as author', () => {
|
||||
// beforeEach(async () => {
|
||||
// authenticatedUser = await author.toJson()
|
||||
// })
|
||||
|
||||
// it('updates a post', async () => {
|
||||
// const expected = {
|
||||
// data: { UpdatePost: { id: 'p9876', content: 'New content' } },
|
||||
// errors: undefined,
|
||||
// }
|
||||
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
|
||||
// it('updates a post, but maintains non-updated attributes', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
|
||||
// },
|
||||
// errors: undefined,
|
||||
// }
|
||||
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
|
||||
// it('updates the updatedAt attribute', async () => {
|
||||
// newlyCreatedPost = await newlyCreatedPost.toJson()
|
||||
// const {
|
||||
// data: { UpdatePost },
|
||||
// } = await mutate({ mutation: updatePostMutation, variables })
|
||||
// expect(newlyCreatedPost.updatedAt).toBeTruthy()
|
||||
// expect(Date.parse(newlyCreatedPost.updatedAt)).toEqual(expect.any(Number))
|
||||
// expect(UpdatePost.updatedAt).toBeTruthy()
|
||||
// expect(Date.parse(UpdatePost.updatedAt)).toEqual(expect.any(Number))
|
||||
// expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
|
||||
// })
|
||||
|
||||
// /* describe('no new category ids provided for update', () => {
|
||||
// it('resolves and keeps current categories', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// UpdatePost: {
|
||||
// id: 'p9876',
|
||||
// categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
|
||||
// },
|
||||
// },
|
||||
// errors: undefined,
|
||||
// }
|
||||
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
// }) */
|
||||
|
||||
// /* describe('given category ids', () => {
|
||||
// beforeEach(() => {
|
||||
// variables = { ...variables, categoryIds: ['cat27'] }
|
||||
// })
|
||||
|
||||
// it('updates categories of a post', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// UpdatePost: {
|
||||
// id: 'p9876',
|
||||
// categories: expect.arrayContaining([{ id: 'cat27' }]),
|
||||
// },
|
||||
// },
|
||||
// errors: undefined,
|
||||
// }
|
||||
// await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
// }) */
|
||||
|
||||
// describe('params.image', () => {
|
||||
// describe('is object', () => {
|
||||
// beforeEach(() => {
|
||||
// variables = { ...variables, image: { sensitive: true } }
|
||||
// })
|
||||
// it('updates the image', async () => {
|
||||
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
// await mutate({ mutation: updatePostMutation, variables })
|
||||
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy()
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('is null', () => {
|
||||
// beforeEach(() => {
|
||||
// variables = { ...variables, image: null }
|
||||
// })
|
||||
// it('deletes the image', async () => {
|
||||
// await expect(neode.all('Image')).resolves.toHaveLength(6)
|
||||
// await mutate({ mutation: updatePostMutation, variables })
|
||||
// await expect(neode.all('Image')).resolves.toHaveLength(5)
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('is undefined', () => {
|
||||
// beforeEach(() => {
|
||||
// delete variables.image
|
||||
// })
|
||||
// it('keeps the image unchanged', async () => {
|
||||
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
// await mutate({ mutation: updatePostMutation, variables })
|
||||
// await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('DeletePost', () => {
|
||||
// let author
|
||||
// const deletePostMutation = gql`
|
||||
// mutation ($id: ID!) {
|
||||
// DeletePost(id: $id) {
|
||||
// id
|
||||
// deleted
|
||||
// content
|
||||
// contentExcerpt
|
||||
// image {
|
||||
// url
|
||||
// }
|
||||
// comments {
|
||||
// deleted
|
||||
// content
|
||||
// contentExcerpt
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
// beforeEach(async () => {
|
||||
// author = await Factory.build('user')
|
||||
// await Factory.build(
|
||||
// 'post',
|
||||
// {
|
||||
// id: 'p4711',
|
||||
// title: 'I will be deleted',
|
||||
// content: 'To be deleted',
|
||||
// },
|
||||
// {
|
||||
// image: Factory.build('image', {
|
||||
// url: 'path/to/some/image',
|
||||
// }),
|
||||
// author,
|
||||
// categoryIds,
|
||||
// },
|
||||
// )
|
||||
// variables = { ...variables, id: 'p4711' }
|
||||
// })
|
||||
|
||||
// describe('unauthenticated', () => {
|
||||
// it('throws authorization error', async () => {
|
||||
// const { errors } = await mutate({ mutation: deletePostMutation, variables })
|
||||
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('authenticated but not the author', () => {
|
||||
// beforeEach(async () => {
|
||||
// authenticatedUser = await user.toJson()
|
||||
// })
|
||||
|
||||
// it('throws authorization error', async () => {
|
||||
// const { errors } = await mutate({ mutation: deletePostMutation, variables })
|
||||
// expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe('authenticated as author', () => {
|
||||
// beforeEach(async () => {
|
||||
// authenticatedUser = await author.toJson()
|
||||
// })
|
||||
|
||||
// it('marks the post as deleted and blacks out attributes', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// DeletePost: {
|
||||
// id: 'p4711',
|
||||
// deleted: true,
|
||||
// content: 'UNAVAILABLE',
|
||||
// contentExcerpt: 'UNAVAILABLE',
|
||||
// image: null,
|
||||
// comments: [],
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// await expect(mutate({ mutation: deletePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
|
||||
// describe('if there are comments on the post', () => {
|
||||
// beforeEach(async () => {
|
||||
// await Factory.build(
|
||||
// 'comment',
|
||||
// {
|
||||
// content: 'to be deleted comment content',
|
||||
// contentExcerpt: 'to be deleted comment content',
|
||||
// },
|
||||
// {
|
||||
// postId: 'p4711',
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
|
||||
// it('marks the comments as deleted', async () => {
|
||||
// const expected = {
|
||||
// data: {
|
||||
// DeletePost: {
|
||||
// id: 'p4711',
|
||||
// deleted: true,
|
||||
// content: 'UNAVAILABLE',
|
||||
// contentExcerpt: 'UNAVAILABLE',
|
||||
// image: null,
|
||||
// comments: [
|
||||
// {
|
||||
// deleted: true,
|
||||
// // Should we black out the comment content in the database, too?
|
||||
// content: 'UNAVAILABLE',
|
||||
// contentExcerpt: 'UNAVAILABLE',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// await expect(mutate({ mutation: deletePostMutation, variables })).resolves.toMatchObject(
|
||||
// expected,
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
@ -21,7 +21,7 @@ type Group {
|
||||
id: ID!
|
||||
name: String # title
|
||||
slug: String!
|
||||
|
||||
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
deleted: Boolean
|
||||
@ -41,12 +41,14 @@ type Group {
|
||||
# Wolle: needed?
|
||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
||||
|
||||
owner: User @relation(name: "OWNS", direction: "IN")
|
||||
|
||||
# Wolle: showShoutsPublicly: Boolean
|
||||
# Wolle: sendNotificationEmails: Boolean
|
||||
# Wolle: needed? locale: String
|
||||
members: [User]! @relation(name: "MEMBERS", direction: "OUT")
|
||||
membersCount: Int!
|
||||
@cypher(statement: "MATCH (this)-[:MEMBERS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||
# members: [User]! @relation(name: "MEMBERS", direction: "OUT")
|
||||
# membersCount: Int!
|
||||
# @cypher(statement: "MATCH (this)-[:MEMBERS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
# Wolle: followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||
# Wolle: followedByCount: Int!
|
||||
@ -207,14 +209,14 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
CreateGroup (
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
id: ID
|
||||
name: String!
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
description: String
|
||||
categoryIds: [ID]
|
||||
# Wolle: add group settings
|
||||
# Wolle:
|
||||
# showShoutsPublicly: Boolean
|
||||
@ -222,10 +224,9 @@ type Mutation {
|
||||
# locale: String
|
||||
): Group
|
||||
|
||||
UpdateUser (
|
||||
UpdateGroup (
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
|
||||
@ -4,3 +4,4 @@ PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
||||
GRAPHQL_URI=http://localhost:4000/
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
@ -33,6 +33,7 @@ const options = {
|
||||
// Cookies
|
||||
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default
|
||||
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user