diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index b1a08a14d..ea4ba3dd2 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -16,11 +16,15 @@ const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && user.role === 'admin'
})
-const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => {
+const isMyOwn = rule({
+ cache: 'no_cache',
+})(async (parent, args, context, info) => {
return context.user.id === parent.id
})
-const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
+const belongsToMe = rule({
+ cache: 'no_cache',
+})(async (_, args, context) => {
const {
driver,
user: { id: userId },
@@ -32,7 +36,10 @@ const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`,
- { userId, notificationId },
+ {
+ userId,
+ notificationId,
+ },
)
const [notification] = result.records.map(record => {
return record.get('n')
@@ -41,12 +48,16 @@ const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
return Boolean(notification)
})
-const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
+const onlyEnabledContent = rule({
+ cache: 'strict',
+})(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
-const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => {
+const isAuthor = rule({
+ cache: 'no_cache',
+})(async (parent, args, { user, driver }) => {
if (!user) return false
const session = driver.session()
const { id: postId } = args
@@ -55,7 +66,9 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver
MATCH (post:Post {id: $postId})<-[:WROTE]-(author)
RETURN author
`,
- { postId },
+ {
+ postId,
+ },
)
const [author] = result.records.map(record => {
return record.get('author')
@@ -100,6 +113,7 @@ const permissions = shield({
enable: isModerator,
disable: isModerator,
CreateComment: isAuthenticated,
+ DeleteComment: isAuthenticated,
// CreateUser: allow,
},
User: {
diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js
index 949b77041..7f3b11da4 100644
--- a/backend/src/resolvers/comments.js
+++ b/backend/src/resolvers/comments.js
@@ -53,6 +53,11 @@ export default {
)
session.close()
+ return comment
+ },
+ DeleteComment: async (object, params, context, resolveInfo) => {
+ const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
+
return comment
},
},
diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js
index 451c559f1..e3bf00528 100644
--- a/backend/src/resolvers/comments.spec.js
+++ b/backend/src/resolvers/comments.spec.js
@@ -1,3 +1,4 @@
+import gql from 'graphql-tag'
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
@@ -5,6 +6,7 @@ import { host, login } from '../jest/helpers'
const factory = Factory()
let client
let createCommentVariables
+let deleteCommentVariables
let createPostVariables
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
@@ -21,22 +23,22 @@ afterEach(async () => {
})
describe('CreateComment', () => {
- const createCommentMutation = `
- mutation($postId: ID, $content: String!) {
- CreateComment(postId: $postId, content: $content) {
- id
- content
+ const createCommentMutation = gql`
+ mutation($postId: ID, $content: String!) {
+ CreateComment(postId: $postId, content: $content) {
+ id
+ content
+ }
}
- }
`
- const createPostMutation = `
- mutation($id: ID!, $title: String!, $content: String!) {
- CreatePost(id: $id, title: $title, content: $content) {
- id
+ const createPostMutation = gql`
+ mutation($id: ID!, $title: String!, $content: String!) {
+ CreatePost(id: $id, title: $title, content: $content) {
+ id
+ }
}
- }
`
- const commentQueryForPostId = `
+ const commentQueryForPostId = gql`
query($content: String) {
Comment(content: $content) {
postId
@@ -59,8 +61,13 @@ describe('CreateComment', () => {
describe('authenticated', () => {
let headers
beforeEach(async () => {
- headers = await login({ email: 'test@example.org', password: '1234' })
- client = new GraphQLClient(host, { headers })
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
createCommentVariables = {
postId: 'p1',
content: "I'm authorised to comment",
@@ -88,15 +95,25 @@ describe('CreateComment', () => {
it('assigns the authenticated user as author', async () => {
await client.request(createCommentMutation, createCommentVariables)
- const { User } = await client.request(`{
+ const { User } = await client.request(gql`
+ {
User(email: "test@example.org") {
comments {
content
}
}
- }`)
+ }
+ `)
- expect(User).toEqual([{ comments: [{ content: "I'm authorised to comment" }] }])
+ expect(User).toEqual([
+ {
+ comments: [
+ {
+ content: "I'm authorised to comment",
+ },
+ ],
+ },
+ ])
})
it('throw an error if an empty string is sent from the editor as content', async () => {
@@ -186,7 +203,92 @@ describe('CreateComment', () => {
commentQueryForPostId,
commentQueryVariablesByContent,
)
- expect(Comment).toEqual([{ postId: null }])
+ expect(Comment).toEqual([
+ {
+ postId: null,
+ },
+ ])
})
})
})
+
+describe('DeleteComment', () => {
+ const createCommentMutation = gql`
+ mutation($postId: ID, $content: String!) {
+ CreateComment(postId: $postId, content: $content) {
+ id
+ content
+ }
+ }
+ `
+ const deleteCommentMutation = gql`
+ mutation($id: ID!) {
+ DeleteComment(id: $id) {
+ id
+ }
+ }
+ `
+ const createPostMutation = gql`
+ mutation($id: ID!, $title: String!, $content: String!) {
+ CreatePost(id: $id, title: $title, content: $content) {
+ id
+ }
+ }
+ `
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ deleteCommentVariables = {
+ id: 'c1',
+ }
+ client = new GraphQLClient(host)
+ await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ createCommentVariables = {
+ id: 'c1',
+ postId: 'p1',
+ content: "I'm authorised to comment",
+ }
+ deleteCommentVariables = {
+ id: 'c1',
+ }
+ createPostVariables = {
+ id: 'p1',
+ title: 'post to comment on',
+ content: 'please comment on me',
+ }
+ await client.request(createPostMutation, createPostVariables)
+ })
+
+ it('deletes the authors comment', async () => {
+ const { CreateComment } = await client.request(createCommentMutation, createCommentVariables)
+
+ deleteCommentVariables = {
+ id: CreateComment.id,
+ }
+ const expected = {
+ DeleteComment: {
+ id: CreateComment.id,
+ },
+ }
+ await expect(
+ client.request(deleteCommentMutation, deleteCommentVariables),
+ ).resolves.toMatchObject(expected)
+ })
+
+ it.todo('throws an error if it tries to delete a comment not from this author')
+ })
+})
diff --git a/backend/src/resolvers/socialMedia.spec.js b/backend/src/resolvers/socialMedia.spec.js
index 5143587b1..4e298e783 100644
--- a/backend/src/resolvers/socialMedia.spec.js
+++ b/backend/src/resolvers/socialMedia.spec.js
@@ -1,13 +1,14 @@
+import gql from 'graphql-tag'
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
-describe('CreateSocialMedia', () => {
+describe('SocialMedia', () => {
let client
let headers
- const mutationC = `
+ const mutationC = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
@@ -15,7 +16,7 @@ describe('CreateSocialMedia', () => {
}
}
`
- const mutationD = `
+ const mutationD = gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
@@ -42,19 +43,28 @@ describe('CreateSocialMedia', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
- const variables = { url: 'http://nsosp.org' }
+ const variables = {
+ url: 'http://nsosp.org',
+ }
await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
beforeEach(async () => {
- headers = await login({ email: 'test@example.org', password: '1234' })
- client = new GraphQLClient(host, { headers })
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
})
it('creates social media with correct URL', async () => {
- const variables = { url: 'http://nsosp.org' }
+ const variables = {
+ url: 'http://nsosp.org',
+ }
await expect(client.request(mutationC, variables)).resolves.toEqual(
expect.objectContaining({
CreateSocialMedia: {
@@ -66,11 +76,15 @@ describe('CreateSocialMedia', () => {
})
it('deletes social media', async () => {
- const creationVariables = { url: 'http://nsosp.org' }
+ const creationVariables = {
+ url: 'http://nsosp.org',
+ }
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
- const deletionVariables = { id }
+ const deletionVariables = {
+ id,
+ }
const expected = {
DeleteSocialMedia: {
id: id,
@@ -81,12 +95,16 @@ describe('CreateSocialMedia', () => {
})
it('rejects empty string', async () => {
- const variables = { url: '' }
+ const variables = {
+ url: '',
+ }
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
it('validates URLs', async () => {
- const variables = { url: 'not-a-url' }
+ const variables = {
+ url: 'not-a-url',
+ }
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
})
diff --git a/webapp/components/Comment.spec.js b/webapp/components/Comment.spec.js
index ebb9b8bf8..e899a05e1 100644
--- a/webapp/components/Comment.spec.js
+++ b/webapp/components/Comment.spec.js
@@ -14,11 +14,20 @@ describe('Comment.vue', () => {
let propsData
let mocks
let getters
+ let wrapper
+ let Wrapper
beforeEach(() => {
propsData = {}
mocks = {
$t: jest.fn(),
+ $toast: {
+ success: jest.fn(),
+ error: jest.fn(),
+ },
+ $apollo: {
+ mutate: jest.fn().mockResolvedValue(),
+ },
}
getters = {
'auth/user': () => {
@@ -29,11 +38,16 @@ describe('Comment.vue', () => {
})
describe('shallowMount', () => {
- const Wrapper = () => {
+ Wrapper = () => {
const store = new Vuex.Store({
getters,
})
- return shallowMount(Comment, { store, propsData, mocks, localVue })
+ return shallowMount(Comment, {
+ store,
+ propsData,
+ mocks,
+ localVue,
+ })
}
describe('given a comment', () => {
@@ -45,7 +59,7 @@ describe('Comment.vue', () => {
})
it('renders content', () => {
- const wrapper = Wrapper()
+ wrapper = Wrapper()
expect(wrapper.text()).toMatch('Hello I am a comment content')
})
@@ -55,17 +69,17 @@ describe('Comment.vue', () => {
})
it('renders no comment data', () => {
- const wrapper = Wrapper()
+ wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('comment content')
})
it('has no "disabled-content" css class', () => {
- const wrapper = Wrapper()
+ wrapper = Wrapper()
expect(wrapper.classes()).not.toContain('disabled-content')
})
it('translates a placeholder', () => {
- /* const wrapper = */ Wrapper()
+ wrapper = Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['comment.content.unavailable-placeholder']]
expect(calls).toEqual(expect.arrayContaining(expected))
@@ -77,16 +91,46 @@ describe('Comment.vue', () => {
})
it('renders comment data', () => {
- const wrapper = Wrapper()
+ wrapper = Wrapper()
expect(wrapper.text()).toMatch('comment content')
})
it('has a "disabled-content" css class', () => {
- const wrapper = Wrapper()
+ wrapper = Wrapper()
expect(wrapper.classes()).toContain('disabled-content')
})
})
})
+
+ beforeEach(jest.useFakeTimers)
+
+ describe('test callbacks', () => {
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
+ beforeEach(() => {
+ wrapper.vm.deleteCommentCallback()
+ })
+
+ describe('after timeout', () => {
+ beforeEach(jest.runAllTimers)
+
+ it('emits "deleteComment"', () => {
+ expect(wrapper.emitted().deleteComment.length).toBe(1)
+ })
+
+ it('does call mutation', () => {
+ expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
+ })
+
+ it('mutation is successful', () => {
+ expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
+ })
+ })
+ })
+ })
})
})
})
diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue
index 751a11f02..862b95545 100644
--- a/webapp/components/Comment.vue
+++ b/webapp/components/Comment.vue
@@ -14,6 +14,7 @@
placement="bottom-end"
resource-type="comment"
:resource="comment"
+ :callbacks="{ confirm: deleteCommentCallback, cancel: null }"
style="float-right"
:is-owner="isAuthor(author.id)"
/>
@@ -27,6 +28,7 @@
diff --git a/webapp/components/ContentMenu.vue b/webapp/components/ContentMenu.vue
index 6e5bca4c0..69289cb6d 100644
--- a/webapp/components/ContentMenu.vue
+++ b/webapp/components/ContentMenu.vue
@@ -28,6 +28,7 @@
import Dropdown from '~/components/Dropdown'
export default {
+ name: 'ContentMenu',
components: {
Dropdown,
},
@@ -42,6 +43,7 @@ export default {
return value.match(/(contribution|comment|organization|user)/)
},
},
+ callbacks: { type: Object, required: true },
},
computed: {
routes() {
@@ -49,7 +51,7 @@ export default {
if (this.isOwner && this.resourceType === 'contribution') {
routes.push({
- name: this.$t(`contribution.edit`),
+ name: this.$t(`post.menu.edit`),
path: this.$router.resolve({
name: 'post-edit-id',
params: {
@@ -59,21 +61,29 @@ export default {
icon: 'edit',
})
routes.push({
- name: this.$t(`post.delete.title`),
+ name: this.$t(`post.menu.delete`),
callback: () => {
this.openModal('delete')
},
icon: 'trash',
})
}
+
if (this.isOwner && this.resourceType === 'comment') {
+ // routes.push({
+ // name: this.$t(`comment.menu.edit`),
+ // callback: () => {
+ // /* eslint-disable-next-line no-console */
+ // console.log('EDIT COMMENT')
+ // },
+ // icon: 'edit'
+ // })
routes.push({
- name: this.$t(`comment.edit`),
+ name: this.$t(`comment.menu.delete`),
callback: () => {
- /* eslint-disable-next-line no-console */
- console.log('EDIT COMMENT')
+ this.openModal('delete')
},
- icon: 'edit',
+ icon: 'trash',
})
}
@@ -125,6 +135,7 @@ export default {
data: {
type: this.resourceType,
resource: this.resource,
+ callbacks: this.callbacks,
},
})
},
diff --git a/webapp/components/Modal.spec.js b/webapp/components/Modal.spec.js
index 52d13c4a0..0e2158e96 100644
--- a/webapp/components/Modal.spec.js
+++ b/webapp/components/Modal.spec.js
@@ -1,5 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Modal from './Modal.vue'
+import DeleteModal from './Modal/DeleteModal.vue'
import DisableModal from './Modal/DisableModal.vue'
import ReportModal from './Modal/ReportModal.vue'
import Vuex from 'vuex'
@@ -29,7 +30,11 @@ describe('Modal.vue', () => {
'modal/SET_OPEN': mutations.SET_OPEN,
},
})
- return mountMethod(Modal, { store, mocks, localVue })
+ return mountMethod(Modal, {
+ store,
+ mocks,
+ localVue,
+ })
}
}
@@ -55,6 +60,7 @@ describe('Modal.vue', () => {
it('initially empty', () => {
wrapper = Wrapper()
+ expect(wrapper.contains(DeleteModal)).toBe(false)
expect(wrapper.contains(DisableModal)).toBe(false)
expect(wrapper.contains(ReportModal)).toBe(false)
})
@@ -69,6 +75,10 @@ describe('Modal.vue', () => {
id: 'c456',
title: 'some title',
},
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
},
}
wrapper = Wrapper()
@@ -83,6 +93,10 @@ describe('Modal.vue', () => {
type: 'contribution',
name: 'some title',
id: 'c456',
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
})
})
@@ -97,23 +111,49 @@ describe('Modal.vue', () => {
it('passes author name to disable modal', () => {
state.data = {
type: 'comment',
- resource: { id: 'c456', author: { name: 'Author name' } },
+ resource: {
+ id: 'c456',
+ author: {
+ name: 'Author name',
+ },
+ },
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
}
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: 'Author name',
id: 'c456',
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
})
})
it('does not crash if author is undefined', () => {
- state.data = { type: 'comment', resource: { id: 'c456' } }
+ state.data = {
+ type: 'comment',
+ resource: {
+ id: 'c456',
+ },
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
+ }
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: '',
id: 'c456',
+ callbacks: {
+ confirm: null,
+ cancel: null,
+ },
})
})
})
diff --git a/webapp/components/Modal.vue b/webapp/components/Modal.vue
index 88b89d407..efe1b8ab6 100644
--- a/webapp/components/Modal.vue
+++ b/webapp/components/Modal.vue
@@ -1,10 +1,12 @@
+