diff --git a/backend/package.json b/backend/package.json index fa44daef8..0de8b69cb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,7 +41,7 @@ ] }, "dependencies": { - "@hapi/joi": "^16.1.1", + "@hapi/joi": "^16.1.2", "@sentry/node": "^5.6.2", "activitystrea.ms": "~2.1.3", "apollo-cache-inmemory": "~1.6.3", @@ -55,38 +55,38 @@ "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~6.0.0", - "date-fns": "2.2.1", + "date-fns": "2.3.0", "debug": "~4.1.1", "dotenv": "~8.1.0", "express": "^4.17.1", "faker": "Marak/faker.js#master", - "graphql": "^14.5.6", + "graphql": "^14.5.7", "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.5", "graphql-middleware-sentry": "^3.2.0", "graphql-shield": "~6.1.0", "graphql-tag": "~2.10.1", - "helmet": "~3.21.0", + "helmet": "~3.21.1", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", "lodash": "~4.17.14", "merge-graphql-schemas": "^1.7.0", "metascraper": "^4.10.3", - "metascraper-audio": "^5.7.4", + "metascraper-audio": "^5.7.5", "metascraper-author": "^5.7.4", "metascraper-clearbit-logo": "^5.3.0", "metascraper-date": "^5.7.4", "metascraper-description": "^5.7.4", - "metascraper-image": "^5.7.4", + "metascraper-image": "^5.7.5", "metascraper-lang": "^5.7.4", "metascraper-lang-detector": "^4.8.5", - "metascraper-logo": "^5.7.4", + "metascraper-logo": "^5.7.5", "metascraper-publisher": "^5.7.4", "metascraper-soundcloud": "^5.7.4", "metascraper-title": "^5.7.4", - "metascraper-url": "^5.7.4", - "metascraper-video": "^5.7.4", + "metascraper-url": "^5.7.5", + "metascraper-video": "^5.7.5", "metascraper-youtube": "^5.7.4", "minimatch": "^3.0.4", "mustache": "^3.0.3", diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 116794fda..ce98090ad 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -152,8 +152,8 @@ const permissions = shield( // RemoveBadgeRewarded: isAdmin, reward: isAdmin, unreward: isAdmin, - follow: isAuthenticated, - unfollow: isAuthenticated, + followUser: isAuthenticated, + unfollowUser: isAuthenticated, shout: isAuthenticated, unshout: isAuthenticated, changePassword: isAuthenticated, diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 736b7b1ab..311e64350 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -4,7 +4,7 @@ module.exports = { id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests actorId: { type: 'string', allow: [null] }, name: { type: 'string', disallow: [null], min: 3 }, - slug: 'string', + slug: { type: 'string', regex: /^[a-z0-9_-]+$/, lowercase: true }, encryptedPassword: 'string', avatar: { type: 'string', allow: [null] }, coverImg: { type: 'string', allow: [null] }, diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js index e00136970..7c4a26c55 100644 --- a/backend/src/models/User.spec.js +++ b/backend/src/models/User.spec.js @@ -18,3 +18,67 @@ describe('role', () => { ) }) }) + +describe('slug', () => { + it('normalizes to lowercase letters', async () => { + const user = await instance.create('User', { slug: 'Matt' }) + await expect(user.toJson()).resolves.toEqual( + expect.objectContaining({ + slug: 'matt', + }), + ) + }) + + it('must be unique', async done => { + await instance.create('User', { slug: 'Matt' }) + try { + await expect(instance.create('User', { slug: 'Matt' })).rejects.toThrow('already exists') + done() + } catch (error) { + throw new Error(` + ${error} + + Probably your database has no unique constraints! + + To see all constraints go to http://localhost:7474/browser/ and + paste the following: + \`\`\` + CALL db.constraints(); + \`\`\` + + Learn how to setup the database here: + https://docs.human-connection.org/human-connection/neo4j + `) + } + }) + + describe('characters', () => { + const createUser = attrs => { + return instance.create('User', attrs).then(user => user.toJson()) + } + + it('-', async () => { + await expect(createUser({ slug: 'matt-rider' })).resolves.toMatchObject({ + slug: 'matt-rider', + }) + }) + + it('_', async () => { + await expect(createUser({ slug: 'matt_rider' })).resolves.toMatchObject({ + slug: 'matt_rider', + }) + }) + + it(' ', async () => { + await expect(createUser({ slug: 'matt rider' })).rejects.toThrow( + /fails to match the required pattern/, + ) + }) + + it('ä', async () => { + await expect(createUser({ slug: 'mätt' })).rejects.toThrow( + /fails to match the required pattern/, + ) + }) + }) +}) diff --git a/backend/src/schema/resolvers/follow.js b/backend/src/schema/resolvers/follow.js index f0a98e1f7..ada417cff 100644 --- a/backend/src/schema/resolvers/follow.js +++ b/backend/src/schema/resolvers/follow.js @@ -4,24 +4,24 @@ const neode = getNeode() export default { Mutation: { - follow: async (_object, params, context, _resolveInfo) => { - const { id: followedId, type } = params + followUser: async (_object, params, context, _resolveInfo) => { + const { id: followedUserId } = params const { user: currentUser } = context - if (type === 'User' && currentUser.id === followedId) { + if (currentUser.id === followedUserId) { return null } - const [user, followedNode] = await Promise.all([ + const [user, followedUser] = await Promise.all([ neode.find('User', currentUser.id), - neode.find(type, followedId), + neode.find('User', followedUserId), ]) - await user.relateTo(followedNode, 'following') - return followedNode.toJson() + await user.relateTo(followedUser, 'following') + return followedUser.toJson() }, - unfollow: async (_object, params, context, _resolveInfo) => { - const { id: followedId, type } = params + unfollowUser: async (_object, params, context, _resolveInfo) => { + const { id: followedUserId } = params const { user: currentUser } = context /* @@ -30,15 +30,14 @@ export default { * However, pure cypher query looks cleaner IMO */ await neode.cypher( - `MATCH (user:User {id: $currentUser.id})-[relation:FOLLOWS]->(node {id: $followedId}) - WHERE $type IN labels(node) + `MATCH (user:User {id: $currentUser.id})-[relation:FOLLOWS]->(followedUser:User {id: $followedUserId}) DELETE relation RETURN COUNT(relation) > 0 as isFollowed`, - { followedId, type, currentUser }, + { followedUserId, currentUser }, ) - const followedNode = await neode.find(type, followedId) - return followedNode.toJson() + const followedUser = await neode.find('User', followedUserId) + return followedUser.toJson() }, }, } diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 1aa146075..7b801d377 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -1,11 +1,12 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' -import { getDriver } from '../../bootstrap/neo4j' +import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' import createServer from '../../server' import { gql } from '../../jest/helpers' const factory = Factory() const driver = getDriver() +const neode = getNeode() let query let mutate @@ -16,8 +17,8 @@ let user2 let variables const mutationFollowUser = gql` - mutation($id: ID!, $type: FollowTypeEnum) { - follow(id: $id, type: $type) { + mutation($id: ID!) { + followUser(id: $id) { name followedBy { id @@ -29,8 +30,8 @@ const mutationFollowUser = gql` ` const mutationUnfollowUser = gql` - mutation($id: ID!, $type: FollowTypeEnum) { - unfollow(id: $id, type: $type) { + mutation($id: ID!) { + unfollowUser(id: $id) { name followedBy { id @@ -52,10 +53,12 @@ const userQuery = gql` } ` -beforeAll(() => { +beforeAll(async () => { + await factory.cleanDatabase() const { server } = createServer({ context: () => ({ driver, + neode, user: authenticatedUser, cypherParams: { currentUserId: authenticatedUser ? authenticatedUser.id : null, @@ -87,7 +90,7 @@ beforeEach(async () => { .then(user => user.toJson()) authenticatedUser = user1 - variables = { id: user2.id, type: 'User' } + variables = { id: user2.id } }) afterEach(async () => { @@ -99,71 +102,85 @@ describe('follow', () => { describe('unauthenticated follow', () => { test('throws authorization error', async () => { authenticatedUser = null - const { errors, data } = await mutate({ - mutation: mutationFollowUser, - variables, + await expect( + mutate({ + mutation: mutationFollowUser, + variables, + }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + data: { followUser: null }, }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') - expect(data).toMatchObject({ follow: null }) }) }) test('I can follow another user', async () => { - const { data: result } = await mutate({ - mutation: mutationFollowUser, - variables, - }) const expectedUser = { name: user2.name, followedBy: [{ id: user1.id, name: user1.name }], followedByCurrentUser: true, } - expect(result).toMatchObject({ follow: expectedUser }) + await expect( + mutate({ + mutation: mutationFollowUser, + variables, + }), + ).resolves.toMatchObject({ + data: { followUser: expectedUser }, + errors: undefined, + }) }) test('I can`t follow myself', async () => { variables.id = user1.id - const { data: result } = await mutate({ mutation: mutationFollowUser, variables }) - const expectedResult = { follow: null } - expect(result).toMatchObject(expectedResult) - - const { data } = await query({ - query: userQuery, - variables: { id: user1.id }, + await expect(mutate({ mutation: mutationFollowUser, variables })).resolves.toMatchObject({ + data: { followUser: null }, + errors: undefined, }) + const expectedUser = { followedBy: [], followedByCurrentUser: false, } - expect(data).toMatchObject({ User: [expectedUser] }) + await expect( + query({ + query: userQuery, + variables: { id: user1.id }, + }), + ).resolves.toMatchObject({ + data: { + User: [expectedUser], + }, + errors: undefined, + }) }) }) describe('unfollow user', () => { beforeEach(async () => { - variables = { - id: user2.id, - type: 'User', - } + variables = { id: user2.id } await mutate({ mutation: mutationFollowUser, variables }) }) describe('unauthenticated follow', () => { test('throws authorization error', async () => { authenticatedUser = null - const { errors, data } = await mutate({ mutation: mutationUnfollowUser, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') - expect(data).toMatchObject({ unfollow: null }) + await expect(mutate({ mutation: mutationUnfollowUser, variables })).resolves.toMatchObject({ + data: { unfollowUser: null }, + errors: [{ message: 'Not Authorised!' }], + }) }) }) - it('I can unfollow a user', async () => { - const { data: result } = await mutate({ mutation: mutationUnfollowUser, variables }) + test('I can unfollow a user', async () => { const expectedUser = { name: user2.name, followedBy: [], followedByCurrentUser: false, } - expect(result).toMatchObject({ unfollow: expectedUser }) + await expect(mutate({ mutation: mutationUnfollowUser, variables })).resolves.toMatchObject({ + data: { unfollowUser: expectedUser }, + errors: undefined, + }) }) }) }) diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index 3107a5799..2ac0dd934 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,420 +1,349 @@ -import { GraphQLClient } from 'graphql-request' +import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +import { gql } from '../../jest/helpers' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' -let client const factory = Factory() -const instance = neode() -const categoryIds = ['cat9'] +const neode = getNeode() +const driver = getDriver() -const setupAuthenticateClient = params => { - const authenticateClient = async () => { - await factory.create('User', params) - const headers = await login(params) - client = new GraphQLClient(host, { headers }) +let query, mutate, authenticatedUser, variables, moderator, nonModerator + +const disableMutation = gql` + mutation($id: ID!) { + disable(id: $id) } - return authenticateClient -} - -let createResource -let authenticateClient -let createPostVariables -let createCommentVariables - -beforeEach(async () => { - createResource = () => {} - authenticateClient = () => { - client = new GraphQLClient(host) +` +const enableMutation = gql` + mutation($id: ID!) { + enable(id: $id) } - await instance.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) -}) +` -const setup = async () => { - await createResource() - await authenticateClient() -} - -afterEach(async () => { - await factory.cleanDatabase() -}) - -describe('disable', () => { - const mutation = gql` - mutation($id: ID!) { - disable(id: $id) +const commentQuery = gql` + query($id: ID!) { + Comment(id: $id) { + id + disabled + disabledBy { + id + } } - ` - let variables - - beforeEach(() => { - // our defaul set of variables - variables = { - id: 'blabla', - } - }) - - const action = async () => { - return client.request(mutation, variables) } +` - it('throws authorization error', async () => { - await setup() - await expect(action()).rejects.toThrow('Not Authorised') +const postQuery = gql` + query($id: ID) { + Post(id: $id) { + id + disabled + disabledBy { + id + } + } + } +` + +describe('moderate resources', () => { + beforeAll(() => { + authenticatedUser = undefined + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate + query = createTestClient(server).query }) - describe('authenticated', () => { + beforeEach(async () => { + variables = {} + authenticatedUser = null + moderator = await factory.create('User', { + id: 'moderator-id', + name: 'Moderator', + email: 'moderator@example.org', + password: '1234', + role: 'moderator', + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('disable', () => { beforeEach(() => { - authenticateClient = setupAuthenticateClient({ - email: 'user@example.org', - password: '1234', + variables = { + id: 'some-resource', + } + }) + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) }) }) - - it('throws authorization error', async () => { - await setup() - await expect(action()).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - beforeEach(() => { - authenticateClient = setupAuthenticateClient({ - id: 'u7', - email: 'moderator@example.org', - password: '1234', - role: 'moderator', + describe('authenticated', () => { + describe('non moderator', () => { + beforeEach(async () => { + nonModerator = await factory.create('User', { + id: 'non-moderator', + name: 'Non Moderator', + email: 'non.moderator@example.org', + password: '1234', + }) + authenticatedUser = await nonModerator.toJson() + }) + it('throws authorization error', async () => { + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) }) }) - describe('on something that is not a (Comment|Post|User) ', () => { + describe('moderator', () => { beforeEach(async () => { - variables = { - id: 't23', - } - createResource = () => { - return Promise.all([factory.create('Tag', { id: 't23' })]) - } + authenticatedUser = await moderator.toJson() }) - it('returns null', async () => { - const expected = { disable: null } - await setup() - await expect(action()).resolves.toEqual(expected) - }) - }) + describe('moderate a resource that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + variables = { + id: 'sample-tag-id', + } + await factory.create('Tag', { id: 'sample-tag-id' }) + }) - describe('on a comment', () => { - beforeEach(async () => { - variables = { - id: 'c47', - } - createPostVariables = { - id: 'p3', - title: 'post to comment on', - content: 'please comment on me', - categoryIds, - } - createCommentVariables = { - id: 'c47', - postId: 'p3', - content: 'this comment was created for this post', - } - createResource = async () => { - await factory.create('User', { - id: 'u45', - email: 'commenter@example.org', - password: '1234', + it('returns null', async () => { + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: null }, }) - const asAuthenticatedUser = await factory.authenticateAs({ - email: 'commenter@example.org', - password: '1234', + }) + }) + + describe('moderate a comment', () => { + beforeEach(async () => { + variables = {} + await factory.create('Comment', { + id: 'comment-id', }) - await asAuthenticatedUser.create('Post', createPostVariables) - await asAuthenticatedUser.create('Comment', createCommentVariables) - } + }) + + it('returns disabled resource id', async () => { + variables = { id: 'comment-id' } + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'comment-id' }, + errors: undefined, + }) + }) + + it('changes .disabledBy', async () => { + variables = { id: 'comment-id' } + const before = { data: { Comment: [{ id: 'comment-id', disabledBy: null }] } } + const expected = { + data: { Comment: [{ id: 'comment-id', disabledBy: { id: 'moderator-id' } }] }, + } + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'comment-id' }, + }) + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + }) + + it('updates .disabled on comment', async () => { + variables = { id: 'comment-id' } + const before = { data: { Comment: [{ id: 'comment-id', disabled: false }] } } + const expected = { data: { Comment: [{ id: 'comment-id', disabled: true }] } } + + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'comment-id' }, + }) + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + }) }) - it('returns disabled resource id', async () => { - const expected = { disable: 'c47' } - await setup() - await expect(action()).resolves.toEqual(expected) - }) - - it('changes .disabledBy', async () => { - const before = { Comment: [{ id: 'c47', disabledBy: null }] } - const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] } - - await setup() - await expect(client.request('{ Comment { id, disabledBy { id } } }')).resolves.toEqual( - before, - ) - await action() - await expect( - client.request('{ Comment(disabled: true) { id, disabledBy { id } } }'), - ).resolves.toEqual(expected) - }) - - it('updates .disabled on comment', async () => { - const before = { Comment: [{ id: 'c47', disabled: false }] } - const expected = { Comment: [{ id: 'c47', disabled: true }] } - - await setup() - await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(before) - await action() - await expect( - client.request('{ Comment(disabled: true) { id disabled } }'), - ).resolves.toEqual(expected) - }) - }) - - describe('on a post', () => { - beforeEach(async () => { - variables = { - id: 'p9', - } - - createResource = async () => { - await factory.create('User', { email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + describe('moderate a post', () => { + beforeEach(async () => { + variables = {} await factory.create('Post', { - id: 'p9', // that's the ID we will look for - categoryIds, + id: 'sample-post-id', }) - } + }) + + it('returns disabled resource id', async () => { + variables = { id: 'sample-post-id' } + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'sample-post-id' }, + }) + }) + + it('changes .disabledBy', async () => { + variables = { id: 'sample-post-id' } + const before = { data: { Post: [{ id: 'sample-post-id', disabledBy: null }] } } + const expected = { + data: { Post: [{ id: 'sample-post-id', disabledBy: { id: 'moderator-id' } }] }, + } + + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'sample-post-id' }, + }) + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + }) + + it('updates .disabled on post', async () => { + const before = { data: { Post: [{ id: 'sample-post-id', disabled: false }] } } + const expected = { data: { Post: [{ id: 'sample-post-id', disabled: true }] } } + variables = { id: 'sample-post-id' } + + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) + await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + data: { disable: 'sample-post-id' }, + }) + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + }) + }) + }) + }) + }) + + describe('enable', () => { + describe('unautenticated user', () => { + it('throws authorization error', async () => { + variables = { id: 'sample-post-id' } + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('authenticated user', () => { + describe('non moderator', () => { + beforeEach(async () => { + nonModerator = await factory.create('User', { + id: 'non-moderator', + name: 'Non Moderator', + email: 'non.moderator@example.org', + password: '1234', + }) + authenticatedUser = await nonModerator.toJson() + }) + it('throws authorization error', async () => { + variables = { id: 'sample-post-id' } + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + describe('moderate a resource that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + await Promise.all([factory.create('Tag', { id: 'sample-tag-id' })]) + }) + + it('returns null', async () => { + await expect( + mutate({ mutation: enableMutation, variables: { id: 'sample-tag-id' } }), + ).resolves.toMatchObject({ + data: { enable: null }, + }) + }) }) - it('returns disabled resource id', async () => { - const expected = { disable: 'p9' } - await setup() - await expect(action()).resolves.toEqual(expected) + describe('moderate a comment', () => { + beforeEach(async () => { + variables = { id: 'comment-id' } + await factory.create('Comment', { + id: 'comment-id', + }) + await mutate({ mutation: disableMutation, variables }) + }) + + it('returns enabled resource id', async () => { + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'comment-id' }, + errors: undefined, + }) + }) + + it('changes .disabledBy', async () => { + const expected = { + data: { Comment: [{ id: 'comment-id', disabledBy: null }] }, + errors: undefined, + } + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'comment-id' }, + errors: undefined, + }) + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + }) + + it('updates .disabled on comment', async () => { + const expected = { + data: { Comment: [{ id: 'comment-id', disabled: false }] }, + errors: undefined, + } + + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'comment-id' }, + errors: undefined, + }) + await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + }) }) - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: null }] } - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + describe('moderate a post', () => { + beforeEach(async () => { + variables = { id: 'post-id' } + await factory.create('Post', { + id: 'post-id', + }) + await mutate({ mutation: disableMutation, variables }) + }) - await setup() - await expect(client.request('{ Post { id, disabledBy { id } } }')).resolves.toEqual( - before, - ) - await action() - await expect( - client.request('{ Post(disabled: true) { id, disabledBy { id } } }'), - ).resolves.toEqual(expected) - }) + it('returns enabled resource id', async () => { + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'post-id' }, + errors: undefined, + }) + }) - it('updates .disabled on post', async () => { - const before = { Post: [{ id: 'p9', disabled: false }] } - const expected = { Post: [{ id: 'p9', disabled: true }] } + it('changes .disabledBy', async () => { + const expected = { + data: { Post: [{ id: 'post-id', disabledBy: null }] }, + errors: undefined, + } + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'post-id' }, + errors: undefined, + }) + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + }) - await setup() - await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(before) - await action() - await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual( - expected, - ) - }) - }) - }) - }) -}) - -describe('enable', () => { - const mutation = gql` - mutation($id: ID!) { - enable(id: $id) - } - ` - let variables - - const action = async () => { - return client.request(mutation, variables) - } - - beforeEach(() => { - // our defaul set of variables - variables = { - id: 'blabla', - } - }) - - it('throws authorization error', async () => { - await setup() - await expect(action()).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - beforeEach(() => { - authenticateClient = setupAuthenticateClient({ - email: 'user@example.org', - password: '1234', - }) - }) - - it('throws authorization error', async () => { - await setup() - await expect(action()).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - beforeEach(async () => { - authenticateClient = setupAuthenticateClient({ - role: 'moderator', - email: 'someuser@example.org', - password: '1234', - }) - }) - - describe('on something that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - variables = { - id: 't23', - } - createResource = () => { - // we cannot create a :DISABLED relationship here - return Promise.all([factory.create('Tag', { id: 't23' })]) - } - }) - - it('returns null', async () => { - const expected = { enable: null } - await setup() - await expect(action()).resolves.toEqual(expected) - }) - }) - - describe('on a comment', () => { - beforeEach(async () => { - variables = { - id: 'c456', - } - createPostVariables = { - id: 'p9', - title: 'post to comment on', - content: 'please comment on me', - categoryIds, - } - createCommentVariables = { - id: 'c456', - postId: 'p9', - content: 'this comment was created for this post', - } - createResource = async () => { - await factory.create('User', { - id: 'u123', - email: 'author@example.org', - password: '1234', - }) - const asAuthenticatedUser = await factory.authenticateAs({ - email: 'author@example.org', - password: '1234', - }) - await asAuthenticatedUser.create('Post', createPostVariables) - await asAuthenticatedUser.create('Comment', createCommentVariables) - - const disableMutation = gql` - mutation { - disable(id: "c456") - } - ` - await factory.mutate(disableMutation) // that's we want to delete - } - }) - - it('returns disabled resource id', async () => { - const expected = { enable: 'c456' } - await setup() - await expect(action()).resolves.toEqual(expected) - }) - - it('changes .disabledBy', async () => { - const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] } - const expected = { Comment: [{ id: 'c456', disabledBy: null }] } - - await setup() - await expect( - client.request('{ Comment(disabled: true) { id, disabledBy { id } } }'), - ).resolves.toEqual(before) - await action() - await expect(client.request('{ Comment { id, disabledBy { id } } }')).resolves.toEqual( - expected, - ) - }) - - it('updates .disabled on post', async () => { - const before = { Comment: [{ id: 'c456', disabled: true }] } - const expected = { Comment: [{ id: 'c456', disabled: false }] } - - await setup() - await expect( - client.request('{ Comment(disabled: true) { id disabled } }'), - ).resolves.toEqual(before) - await action() // this updates .disabled - await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(expected) - }) - }) - - describe('on a post', () => { - beforeEach(async () => { - variables = { - id: 'p9', - } - - createResource = async () => { - await factory.create('User', { - id: 'u123', - email: 'author@example.org', - password: '1234', - }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9', // that's the ID we will look for - categoryIds, - }) - - const disableMutation = gql` - mutation { - disable(id: "p9") - } - ` - await factory.mutate(disableMutation) // that's we want to delete - } - }) - - it('returns disabled resource id', async () => { - const expected = { enable: 'p9' } - await setup() - await expect(action()).resolves.toEqual(expected) - }) - - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } - const expected = { Post: [{ id: 'p9', disabledBy: null }] } - - await setup() - await expect( - client.request('{ Post(disabled: true) { id, disabledBy { id } } }'), - ).resolves.toEqual(before) - await action() - await expect(client.request('{ Post { id, disabledBy { id } } }')).resolves.toEqual( - expected, - ) - }) - - it('updates .disabled on post', async () => { - const before = { Post: [{ id: 'p9', disabled: true }] } - const expected = { Post: [{ id: 'p9', disabled: false }] } - - await setup() - await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual( - before, - ) - await action() // this updates .disabled - await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(expected) + it('updates .disabled on post', async () => { + const expected = { + data: { Post: [{ id: 'post-id', disabled: false }] }, + errors: undefined, + } + + await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + data: { enable: 'post-id' }, + errors: undefined, + }) + await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + }) }) }) }) diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 0e4fc48f7..1b9cac102 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -192,7 +192,7 @@ describe('given some notifications', () => { it('returns only unread notifications of current user', async () => { const expected = expect.objectContaining({ data: { - notifications: [ + notifications: expect.arrayContaining([ { from: { __typename: 'Comment', @@ -209,12 +209,15 @@ describe('given some notifications', () => { read: false, createdAt: '2019-08-31T17:33:48.651Z', }, - ], + ]), }, }) - await expect( - query({ query: notificationQuery, variables: { ...variables, read: false } }), - ).resolves.toEqual(expected) + const response = await query({ + query: notificationQuery, + variables: { ...variables, read: false }, + }) + await expect(response).toMatchObject(expected) + await expect(response.data.notifications.length).toEqual(2) // double-check }) describe('if a resource gets deleted', () => { diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 620039b28..06d0e061b 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -31,10 +31,8 @@ type Mutation { shout(id: ID!, type: ShoutTypeEnum): Boolean! # Unshout the given Type and ID unshout(id: ID!, type: ShoutTypeEnum): Boolean! - # Follow the given Type and ID - follow(id: ID!, type: FollowTypeEnum): User - # Unfollow the given Type and ID - unfollow(id: ID!, type: FollowTypeEnum): User + followUser(id: ID!): User + unfollowUser(id: ID!): User } type Report { @@ -57,9 +55,6 @@ enum Deletable { enum ShoutTypeEnum { Post } -enum FollowTypeEnum { - User -} type Reward { id: ID! diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index c0a684715..ab09b438d 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -118,13 +118,12 @@ export default function Factory(options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, - async follow(properties) { - const { id, type } = properties + async followUser(properties) { + const { id } = properties const mutation = ` mutation { - follow( - id: "${id}", - type: ${type} + followUser( + id: "${id}" ) } ` @@ -166,7 +165,7 @@ export default function Factory(options = {}) { result.relate.bind(result) result.mutate.bind(result) result.shout.bind(result) - result.follow.bind(result) + result.followUser.bind(result) result.invite.bind(result) result.cleanDatabase.bind(result) return result diff --git a/backend/yarn.lock b/backend/yarn.lock index 4f381e812..095b9d68f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -766,10 +766,10 @@ "@hapi/hoek" "8.x.x" "@hapi/topo" "3.x.x" -"@hapi/joi@^16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.1.tgz#67a47cf46b163782ab69802b17ac4a86fb89f83e" - integrity sha512-v/XNMGNz+Nx7578Cx2bMunoQHuY4LFxRltJ6uA1LjS6LWakgPCJC4MTr1ucfCnjjbDtaQizrQx9oWXY3WcFcyw== +"@hapi/joi@^16.1.2": + version "16.1.2" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.2.tgz#c566d9e0d81d6847f7622f7d5e23adadaa2d7332" + integrity sha512-wkMIEMQQPNmat9P7zws7wO8Gon9W3NgG5Pac1m0LK8bQ1bbszofxzL0CJogAgzitk5rZZw5/txR+wOK/ioLmGw== dependencies: "@hapi/address" "^2.1.1" "@hapi/formula" "^1.2.0" @@ -971,10 +971,10 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@metascraper/helpers@^5.7.4": - version "5.7.4" - resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.7.4.tgz#c91c1b11ce585fa973a544a9d24c5d88d50a9354" - integrity sha512-GMLFu8j7e65n04w+dfOVF8RWOqNHCqimITtTHYSa1XdLR8vSqE2PjvSOhGoS5ELU5fRlRQKy9EOrKDeRV3/K0w== +"@metascraper/helpers@^5.7.4", "@metascraper/helpers@^5.7.5": + version "5.7.5" + resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.7.5.tgz#fb4ca0e2825f836f1398dcc85227443a36eeb84c" + integrity sha512-ayeJIJqlqeiJHYPYi7fmhjvOg7FHTjfqd57nZCLo0fkqj2exsCa788G5Ihk5qHsk1ASVOgH+flp1XeyMl1vcXQ== dependencies: audio-extensions "0.0.0" chrono-node "~1.3.11" @@ -990,7 +990,7 @@ lodash "~4.17.15" mem "~5.1.1" mime-types "~2.1.24" - normalize-url "~4.3.0" + normalize-url "~4.4.1" smartquotes "~2.3.1" title "~3.4.1" truncate "~2.1.0" @@ -2169,10 +2169,10 @@ boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bowser@2.5.4: - version "2.5.4" - resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.5.4.tgz#850fccfebde92165440279b5ab19be3c7f05cfe1" - integrity sha512-74GGwfc2nzYD19JCiA0RwCxdq7IY5jHeEaSrrgm/5kusEuK+7UK0qDG3gyzN47c4ViNyO4osaKtZE+aSV6nlpQ== +bowser@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.6.1.tgz#196599588af6f0413449c79ab3bf7a5a1bb3384f" + integrity sha512-hySGUuLhi0KetfxPZpuJOsjM0kRvCiCgPBygBkzGzJNsq/nbJmaO8QJc6xlWfeFFnMvtd/LeKkhDJGVrmVobUA== boxen@^1.2.1: version "1.3.0" @@ -2868,10 +2868,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.2.1.tgz#b3f79cf56760af106050c686f4c72586a3383ee9" - integrity sha512-4V1i5CnTinjBvJpXTq7sDHD4NY6JPcl15112IeSNNLUWQOQ+kIuCvRGOFZMQZNvkadw8F9QTyZxz59rIRU6K+w== +date-fns@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.3.0.tgz#017eae725d0c46173b572da025fb5e4e534270fd" + integrity sha512-A8o+iXBVqQayl9Z39BHgb7m/zLOfhF7LK82t+n9Fq1adds1vaUn8ByVoADqWLe4OTc6BZYc/FdbdTwufNYqkJw== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -4209,10 +4209,10 @@ graphql-upload@^8.0.2: http-errors "^1.7.2" object-path "^0.11.4" -graphql@^14.2.1, graphql@^14.5.6: - version "14.5.6" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.6.tgz#3fa12173b50e6ccdef953c31c82f37c50ef58bec" - integrity sha512-zJ6Oz8P1yptV4O4DYXdArSwvmirPetDOBnGFRBl0zQEC68vNW3Ny8qo8VzMgfr+iC8PKiRYJ+f2wub41oDCoQg== +graphql@^14.2.1, graphql@^14.5.7: + version "14.5.7" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.7.tgz#8646a3fcc07922319cc3967eba4a64b32929f77f" + integrity sha512-as410RMJSUFqF8RcH2QWxZ5ioqHzsH9VWnWbaU+UnDXJ/6azMDIYPrtXCBPXd8rlunEVb7W8z6fuUnNHMbFu9A== dependencies: iterall "^1.2.2" @@ -4333,20 +4333,20 @@ helmet-crossdomain@0.4.0: resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e" integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA== -helmet-csp@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.1.tgz#39939a84ca3657ee3cba96f296169ccab02f97d5" - integrity sha512-HgdXSJ6AVyXiy5ohVGpK6L7DhjI9KVdKVB1xRoixxYKsFXFwoVqtLKgDnfe3u8FGGKf9Ml9k//C9rnncIIAmyA== +helmet-csp@2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.2.tgz#bec0adaf370b0f2e77267c9d8b6e33b34159c1e5" + integrity sha512-Lt5WqNfbNjEJ6ysD4UNpVktSyjEKfU9LVJ1LaFmPfYseg/xPealPfgHhtqdAdjPDopp5zbg/VWCyp4cluMIckw== dependencies: - bowser "2.5.4" + bowser "^2.6.1" camelize "1.0.0" content-security-policy-builder "2.1.0" dasherize "2.0.0" -helmet@~3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.0.tgz#e7c5e2ed3b8b7f42d2e387004a87198b295132cc" - integrity sha512-TS3GryQMPR7n/heNnGC0Cl3Ess30g8C6EtqZyylf+Y2/kF4lM8JinOR90rzIICsw4ymWTvji4OhDmqsqxkLrcg== +helmet@~3.21.1: + version "3.21.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.1.tgz#b0ab7c63fc30df2434be27e7e292a9523b3147e9" + integrity sha512-IC/54Lxvvad2YiUdgLmPlNFKLhNuG++waTF5KPYq/Feo3NNhqMFbcLAlbVkai+9q0+4uxjxGPJ9bNykG+3zZNg== dependencies: depd "2.0.0" dns-prefetch-control "0.2.0" @@ -4355,7 +4355,7 @@ helmet@~3.21.0: feature-policy "0.3.0" frameguard "3.1.0" helmet-crossdomain "0.4.0" - helmet-csp "2.9.1" + helmet-csp "2.9.2" hide-powered-by "1.1.0" hpkp "2.0.0" hsts "2.2.0" @@ -5883,12 +5883,12 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -metascraper-audio@^5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.7.4.tgz#a476ed484b2642060208243843dc7ef5c9eb7a3e" - integrity sha512-5M+C+tirJlR71PXymAkBnEXu8KG0+fkX3G0Dm2UO6jDEchIo+DhW2aGSgB8w9kQN80Ni8jaLjNH3+Mtwbsbbkw== +metascraper-audio@^5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.7.5.tgz#9ccdc85a2e17b6767e91ecfc964faaa83e10e917" + integrity sha512-2uE2VrsB780krOoKSGM08iquxyZmLEWNEG/8P3+wbZJ3aQA+JVTc7He/D8XMhFd93dFTpVZUNV9qLlPIjWnwnw== dependencies: - "@metascraper/helpers" "^5.7.4" + "@metascraper/helpers" "^5.7.5" metascraper-author@^5.7.4: version "5.7.4" @@ -5919,12 +5919,12 @@ metascraper-description@^5.7.4: dependencies: "@metascraper/helpers" "^5.7.4" -metascraper-image@^5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.7.4.tgz#095aad47efa263c872d1762fdb3fdc1fcad62129" - integrity sha512-AZRQR9Z6BMJ/EfPG2g5XlRVrkGwiHAPJiakJl1kASHAvfqdznkW6ZjOCta1Wx76x++jjwWRxF67K/elLg8AMdg== +metascraper-image@^5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.7.5.tgz#fef461b706885f6a6be4141e8270318dbc66936d" + integrity sha512-n6SLTCKNugEJuZWHxEISsLOmQKlxs1Rzl+EsZzYeLKYu5fnCI7XegepOC85erofPl3OaivrKyWk3WKUN+qQ3JA== dependencies: - "@metascraper/helpers" "^5.7.4" + "@metascraper/helpers" "^5.7.5" metascraper-lang-detector@^4.8.5: version "4.10.2" @@ -5942,12 +5942,12 @@ metascraper-lang@^5.7.4: dependencies: "@metascraper/helpers" "^5.7.4" -metascraper-logo@^5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.7.4.tgz#a90136718b7f827ba41249442f48a0535245bf13" - integrity sha512-SIpKMWydmVHSFjV7/exPxDx7Ydgp5n5GG0dLBNKCEuv3fHiMulrtevDlV+yk4xIGPh1CnA0hCS6mL7N/2y9ltw== +metascraper-logo@^5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.7.5.tgz#90f9fc30191a495f439e4f36d90af01fd3995a64" + integrity sha512-L+ZyJx+c7V0RyRubr6hITlnTjmEkPVJmXnWHz/bbWXEI++MA8/jI/XVsbxugcliMhdG8/UW+wANZ/uBoRHejdA== dependencies: - "@metascraper/helpers" "^5.7.4" + "@metascraper/helpers" "^5.7.5" metascraper-publisher@^5.7.4: version "5.7.4" @@ -5973,19 +5973,19 @@ metascraper-title@^5.7.4: "@metascraper/helpers" "^5.7.4" lodash "~4.17.15" -metascraper-url@^5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.7.4.tgz#c2aa19d5ebd1e29d1d4154d350cc903fd1725d95" - integrity sha512-ApmaiKny0stNXoGABVDFaXpfK2J5cO/wTUuiaS/bsPWwnwn9TFfdAzatEdzDM6pq77pbKWI6CkdEpeNE5b10/g== +metascraper-url@^5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.7.5.tgz#f503820e2429036b26f5dad0b55f0e430bd49a6d" + integrity sha512-yY1HUiqZf7PkTMN4DeUfxDhtnMCzqyj7IvGbAVM0dHWzxC+s+RNM5NR1jo+DxVIVUxRygo3REQHwVA0Uu1CATg== dependencies: - "@metascraper/helpers" "^5.7.4" + "@metascraper/helpers" "^5.7.5" -metascraper-video@^5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.7.4.tgz#33895606b5bde9199e02c726811925a52f9aefb0" - integrity sha512-8Rm+y0MW+nGS5A5Z08ZAkcwBif60IGNxf7w0D83i1lw5/8K/g/WpGK0NeT8UuVha0ZHXMQcY1TQOhZO56dpAbA== +metascraper-video@^5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.7.5.tgz#15dd760fe26acb21cac7ced60f1ad508b0f130d1" + integrity sha512-LZFSttRIvUz9yEM17Z8CN0XI925CFTrV6pHMMSglD3bQH4qtrne1d+xXDUz6riPhBuR80BA5Xb9OrpRPSNCK2w== dependencies: - "@metascraper/helpers" "^5.7.4" + "@metascraper/helpers" "^5.7.5" lodash "~4.17.15" metascraper-youtube@^5.7.4: @@ -6409,7 +6409,7 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^4.1.0, normalize-url@~4.3.0: +normalize-url@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== @@ -6419,6 +6419,11 @@ normalize-url@~4.2.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.2.0.tgz#e747f16b58e6d7f391495fd86415fa04ec7c9897" integrity sha512-n69+KXI+kZApR+sPwSkoAXpGlNkaiYyoHHqKOFPjJWvwZpew/EjKvuPE4+tStNgb42z5yLtdakgZCQI+LalSPg== +normalize-url@~4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.4.1.tgz#81e9c153b0ad5743755696f2aa20488d48e962b6" + integrity sha512-rjH3yRt0Ssx19mUwS0hrDUOdG9VI+oRLpLHJ7tXRdjcuQ7v7wo6qPvOZppHRrqfslTKr0L2yBhjj4UXd7c3cQg== + npm-bundled@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index b32924f6a..563d6a733 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -18,6 +18,8 @@ When('I save {string} as my new name', name => { cy.get('[type=submit]') .click() .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') }) When('I save {string} as my location', location => { @@ -28,6 +30,8 @@ When('I save {string} as my location', location => { cy.get('[type=submit]') .click() .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') myLocation = location }) @@ -38,6 +42,8 @@ When('I have the following self-description:', text => { cy.get('[type=submit]') .click() .not('[disabled]') + cy.get('.iziToast-message') + .should('contain', 'Your data was successfully updated') aboutMeText = text }) diff --git a/package.json b/package.json index 841dd171a..d71a399b3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "bcryptjs": "^2.4.3", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "cross-env": "^6.0.0", "cypress": "^3.4.1", "cypress-cucumber-preprocessor": "^1.16.0", diff --git a/webapp/.eslintignore b/webapp/.eslintignore index d56900caf..be90fc8e3 100644 --- a/webapp/.eslintignore +++ b/webapp/.eslintignore @@ -3,3 +3,4 @@ build .nuxt styleguide/ **/*.min.js +static/sw.js diff --git a/webapp/.gitignore b/webapp/.gitignore index f8c980f7c..dca219bb5 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -84,3 +84,5 @@ cypress.env.json # Apple macOS folder attribute file .DS_Store + +sw.* diff --git a/webapp/components/Comment.spec.js b/webapp/components/Comment/Comment.spec.js similarity index 99% rename from webapp/components/Comment.spec.js rename to webapp/components/Comment/Comment.spec.js index b2d6d060a..381d49bc2 100644 --- a/webapp/components/Comment.spec.js +++ b/webapp/components/Comment/Comment.spec.js @@ -30,6 +30,7 @@ describe('Comment.vue', () => { }, $filters: { truncate: a => a, + removeHtml: a => a, }, $apollo: { mutate: jest.fn().mockResolvedValue({ diff --git a/webapp/components/Comment/Comment.story.js b/webapp/components/Comment/Comment.story.js new file mode 100644 index 000000000..291b6cb11 --- /dev/null +++ b/webapp/components/Comment/Comment.story.js @@ -0,0 +1,54 @@ +import { storiesOf } from '@storybook/vue' +import { withA11y } from '@storybook/addon-a11y' +import Comment from './Comment' +import helpers from '~/storybook/helpers' + +helpers.init() + +const comment = { + id: '5d42a2277f2725002a449cb3', + content: + '
Thank you all!
@wolfgang-huss @robert-schafer @greg @human-connection
watch my video
I think we can all learn a lot from Alex\'s video :)
It\'s really great stuff!!
Please give him a big smiley face emoticon :D
', + contentExcerpt: + 'Thank you all!
@wolfgang-huss @robert-schafer @greg @human-connection
watch my video
I think we can all learn a lot from Alex\'s video :)
It\'s really great stuff!!
Please give him a …
', + createdAt: '2019-08-01T08:26:15.839Z', + updatedAt: '2019-08-01T08:26:15.839Z', + deleted: false, + disabled: false, + author: { + id: '1', + avatar: + 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg', + slug: 'jenny-rostock', + name: 'Rainer Unsinn', + disabled: false, + deleted: false, + contributionsCount: 25, + shoutedCount: 5, + commentedCount: 39, + followedByCount: 2, + followedByCurrentUser: true, + location: null, + badges: [ + { + id: 'indiegogo_en_bear', + icon: '/img/badges/indiegogo_en_bear.svg', + __typename: 'Badge', + }, + ], + __typename: 'User', + }, + __typename: 'Comment', +} + +storiesOf('Comment', module) + .addDecorator(withA11y) + .addDecorator(helpers.layout) + .add('Basic comment', () => ({ + components: { Comment }, + store: helpers.store, + data: () => ({ + comment, + }), + template: `{{ PostsEmotionsCountByEmotion[emotion] }}x
- {{ $t('contribution.emotions-label.emoted') }}