diff --git a/.github/workflows/publish-branded.yml b/.github/workflows/publish-branded.yml index a404453e6..869eb6302 100644 --- a/.github/workflows/publish-branded.yml +++ b/.github/workflows/publish-branded.yml @@ -46,6 +46,11 @@ jobs: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref }} + - name: Download Docker Image (Backend) uses: actions/download-artifact@v2 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 01256d719..4452f2286 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -293,7 +293,7 @@ jobs: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: Repository Dispatch - uses: peter-evans/repository-dispatch@v1 + uses: peter-evans/repository-dispatch@v2 with: token: ${{ github.token }} event-type: trigger-build-success diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46d80241f..1740c09fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -329,19 +329,16 @@ jobs: - name: backend | docker-compose run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend - name: cypress | Fullstack tests + id: e2e-tests run: | yarn install yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) ########################################################################## - # UPLOAD SCREENSHOTS & VIDEO ############################################# + # UPLOAD SCREENSHOTS - IF TESTS FAIL ##################################### ########################################################################## - - name: Upload Artifact + - name: Full stack tests | if any test failed, upload screenshots + if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} uses: actions/upload-artifact@v3 with: name: cypress-screenshots path: cypress/screenshots/ - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: cypress-videos - path: cypress/videos/ diff --git a/README.md b/README.md index 8ddf97fe5..06e8d4929 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ Prepare database once before you start by running the following command in a sec ```bash # in main folder while docker-compose is up -$ docker-compose exec backend yarn run db:migrate init +$ docker compose exec backend yarn run db:migrate init +$ docker compose exec backend yarn run db:migrate up ``` Then clear and seed database by running the following command as well in the second terminal: diff --git a/backend/README.md b/backend/README.md index 03724ce54..98b0e7218 100644 --- a/backend/README.md +++ b/backend/README.md @@ -81,8 +81,7 @@ More details about our GraphQL playground and how to use it with ocelot.social c ### Database Indexes and Constraints -Database indexes and constraints need to be created when the database and the -backend is running: +Database indexes and constraints need to be created and upgraded when the database and the backend are running: {% tabs %} {% tab title="Docker" %} @@ -98,6 +97,11 @@ $ docker compose exec backend yarn prod:migrate init $ docker compose exec backend /bin/sh -c "yarn prod:migrate init" ``` +```bash +# in main folder with docker compose running +$ docker exec backend yarn run db:migrate up +``` + {% endtab %} {% tab title="Without Docker" %} @@ -107,6 +111,11 @@ $ docker compose exec backend /bin/sh -c "yarn prod:migrate init" yarn run db:migrate init ``` +```bash +# in backend/ with database running (In docker or local) +yarn run db:migrate up +``` + {% endtab %} {% endtabs %} @@ -134,6 +143,8 @@ $ docker exec backend yarn run db:reset $ docker-compose down -v # if container is not running, run this command to set up your database indexes and constraints $ docker exec backend yarn run db:migrate init +# And then upgrade the indexes and const +$ docker exec backend yarn run db:migrate up ``` {% endtab %} diff --git a/backend/src/graphql/groups.js b/backend/src/graphql/groups.js index e388b2cd9..a7cfc3351 100644 --- a/backend/src/graphql/groups.js +++ b/backend/src/graphql/groups.js @@ -150,6 +150,19 @@ export const changeGroupMemberRoleMutation = () => { ` } +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} + // ------ queries export const groupQuery = () => { diff --git a/backend/src/graphql/notifications.js b/backend/src/graphql/notifications.js new file mode 100644 index 000000000..233077372 --- /dev/null +++ b/backend/src/graphql/notifications.js @@ -0,0 +1,65 @@ +import gql from 'graphql-tag' + +// ------ mutations + +export const markAsReadMutation = () => { + return gql` + mutation ($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} + +export const markAllAsReadMutation = () => { + return gql` + mutation { + markAllAsRead { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} + +// ------ queries + +export const notificationQuery = () => { + return gql` + query ($read: Boolean, $orderBy: NotificationOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 9aef8646b..6cd8f39d6 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -253,6 +253,42 @@ const isMemberOfGroup = rule({ } }) +const canRemoveUserFromGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { groupId, userId } = args + const currentUserId = user.id + if (currentUserId === userId) return false + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId}) + OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(user:User { id: $userId }) + RETURN currentUserMembership.role AS currentUserRole, userMembership.role AS userRole + `, + { currentUserId, groupId, userId }, + ) + return { + currentUserRole: transactionResponse.records.map((record) => + record.get('currentUserRole'), + )[0], + userRole: transactionResponse.records.map((record) => record.get('userRole'))[0], + } + }) + try { + const { currentUserRole, userRole } = await readTxPromise + return ( + currentUserRole && ['owner'].includes(currentUserRole) && userRole && userRole !== 'owner' + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -382,6 +418,7 @@ export default shield( JoinGroup: isAllowedToJoinGroup, LeaveGroup: isAllowedToLeaveGroup, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, + RemoveUserFromGroup: canRemoveUserFromGroup, CreatePost: and(isAuthenticated, isMemberOfGroup), UpdatePost: isAuthor, DeletePost: isAuthor, @@ -412,6 +449,7 @@ export default shield( blockUser: isAuthenticated, unblockUser: isAuthenticated, markAsRead: isAuthenticated, + markAllAsRead: isAuthenticated, AddEmailAddress: isAuthenticated, VerifyEmailAddress: isAuthenticated, pinPost: isAdmin, diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4ea588d28..5ec1700b9 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -16,6 +16,7 @@ export default { Group: async (_object, params, context, _resolveInfo) => { const { isMember, id, slug, first, offset } = params let pagination = '' + const orderBy = 'ORDER BY group.createdAt DESC' if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}` const matchParams = { id, slug } removeUndefinedNullValuesFromObject(matchParams) @@ -29,6 +30,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${orderBy} ${pagination} ` } else { @@ -39,6 +41,7 @@ export default { WITH group WHERE group.groupType IN ['public', 'closed'] RETURN group {.*, myRole: NULL} + ${orderBy} ${pagination} ` } else { @@ -48,6 +51,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${orderBy} ${pagination} ` } @@ -295,25 +299,8 @@ export default { LeaveGroup: async (_parent, params, context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const leaveGroupCypher = ` - MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) - DELETE membership - WITH member, group - OPTIONAL MATCH (p:Post)-[:IN]->(group) - WHERE NOT group.groupType = 'public' - WITH member, group, collect(p) AS posts - FOREACH (post IN posts | - MERGE (member)-[:CANNOT_SEE]->(post)) - RETURN member {.*, myRoleInGroup: NULL} - ` - - const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) - return member - }) try { - return await writeTxResultPromise + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) } catch (error) { throw new Error(error) } finally { @@ -368,6 +355,17 @@ export default { session.close() } }, + RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { + const { groupId, userId } = params + const session = context.driver.session() + try { + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Group: { ...Resolver('Group', { @@ -383,3 +381,27 @@ export default { }), }, } + +const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => { + return session.writeTransaction(async (transaction) => { + const removeUserFromGroupCypher = ` + MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + DELETE membership + WITH user, group + OPTIONAL MATCH (author:User)-[:WROTE]->(p:Post)-[:IN]->(group) + WHERE NOT group.groupType = 'public' + AND NOT author.id = $userId + WITH user, collect(p) AS posts + FOREACH (post IN posts | + MERGE (user)-[:CANNOT_SEE]->(post)) + RETURN user {.*, myRoleInGroup: NULL} + ` + + const transactionResponse = await transaction.run(removeUserFromGroupCypher, { + groupId, + userId, + }) + const [user] = await transactionResponse.records.map((record) => record.get('user')) + return user + }) +} diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 3b84f4b42..13291383d 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -6,6 +6,7 @@ import { joinGroupMutation, leaveGroupMutation, changeGroupMemberRoleMutation, + removeUserFromGroupMutation, groupMembersQuery, groupQuery, } from '../../graphql/groups' @@ -196,7 +197,6 @@ const seedComplexScenarioAndClearAuthentication = async () => { }, }) // hidden-group - authenticatedUser = await adminMemberUser.toJson() await mutate({ mutation: createGroupMutation(), variables: { @@ -214,32 +214,17 @@ const seedComplexScenarioAndClearAuthentication = async () => { mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', - userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', + userId: 'usual-member-user', roleInGroup: 'usual', }, }) + await mutate({ mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', - roleInGroup: 'usual', + roleInGroup: 'admin', }, }) @@ -2982,4 +2967,192 @@ describe('in mode', () => { }) }) }) + + describe('RemoveUserFromGroup', () => { + beforeAll(async () => { + await seedComplexScenarioAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('authenticated', () => { + describe('as usual member', () => { + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as owner', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as admin', () => { + beforeEach(async () => { + authenticatedUser = await adminMemberUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + roleInGroup: 'usual', + }, + }) + }) + + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + /* + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + it('cannot remove owner', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + */ + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 3c01ddb97..a2b850336 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -99,6 +99,35 @@ export default { session.close() } }, + markAllAsRead: async (parent, args, context, resolveInfo) => { + const { user: currentUser } = context + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const markAllNotificationAsReadTransactionResponse = await transaction.run( + ` + MATCH (resource)-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) + SET notification.read = TRUE + WITH user, notification, resource, + [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, + [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts + WITH resource, user, notification, authors, posts, + resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource + RETURN notification {.*, from: finalResource, to: properties(user)} + `, + { id: currentUser.id }, + ) + log(markAllNotificationAsReadTransactionResponse) + return markAllNotificationAsReadTransactionResponse.records.map((record) => + record.get('notification'), + ) + }) + try { + const notifications = await writeTxResultPromise + return notifications + } finally { + session.close() + } + }, }, NOTIFIED: { id: async (parent) => { diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 36bd530eb..47134aea6 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -3,6 +3,11 @@ import gql from 'graphql-tag' import { getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' +import { + markAsReadMutation, + markAllAsReadMutation, + notificationQuery, +} from '../../graphql/notifications' const driver = getDriver() let authenticatedUser @@ -146,26 +151,9 @@ describe('given some notifications', () => { }) describe('notifications', () => { - const notificationQuery = gql` - query ($read: Boolean, $orderBy: NotificationOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { - const { errors } = await query({ query: notificationQuery }) + const { errors } = await query({ query: notificationQuery() }) expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -212,7 +200,7 @@ describe('given some notifications', () => { }, ] - await expect(query({ query: notificationQuery, variables })).resolves.toMatchObject({ + await expect(query({ query: notificationQuery(), variables })).resolves.toMatchObject({ data: { notifications: expect.arrayContaining(expected), }, @@ -246,7 +234,7 @@ describe('given some notifications', () => { }, }) const response = await query({ - query: notificationQuery, + query: notificationQuery(), variables: { ...variables, read: false }, }) await expect(response).toMatchObject(expected) @@ -275,14 +263,14 @@ describe('given some notifications', () => { it('reduces notifications list', async () => { await expect( - query({ query: notificationQuery, variables: { ...variables, read: false } }), + query({ query: notificationQuery(), variables: { ...variables, read: false } }), ).resolves.toMatchObject({ data: { notifications: [expect.any(Object), expect.any(Object)] }, errors: undefined, }) await deletePostAction() await expect( - query({ query: notificationQuery, variables: { ...variables, read: false } }), + query({ query: notificationQuery(), variables: { ...variables, read: false } }), ).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined }) }) }) @@ -291,27 +279,10 @@ describe('given some notifications', () => { }) describe('markAsRead', () => { - const markAsReadMutation = gql` - mutation ($id: ID!) { - markAsRead(id: $id) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { const result = await mutate({ - mutation: markAsReadMutation, + mutation: markAsReadMutation(), variables: { ...variables, id: 'p1' }, }) expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') @@ -332,7 +303,7 @@ describe('given some notifications', () => { }) it('returns null', async () => { - const response = await mutate({ mutation: markAsReadMutation, variables }) + const response = await mutate({ mutation: markAsReadMutation(), variables }) expect(response.data.markAsRead).toEqual(null) expect(response.errors).toBeUndefined() }) @@ -348,7 +319,7 @@ describe('given some notifications', () => { }) it('updates `read` attribute and returns NOTIFIED relationship', async () => { - const { data } = await mutate({ mutation: markAsReadMutation, variables }) + const { data } = await mutate({ mutation: markAsReadMutation(), variables }) expect(data).toEqual({ markAsRead: { from: { @@ -369,7 +340,7 @@ describe('given some notifications', () => { } }) it('returns null', async () => { - const response = await mutate({ mutation: markAsReadMutation, variables }) + const response = await mutate({ mutation: markAsReadMutation(), variables }) expect(response.data.markAsRead).toEqual(null) expect(response.errors).toBeUndefined() }) @@ -385,7 +356,7 @@ describe('given some notifications', () => { }) it('updates `read` attribute and returns NOTIFIED relationship', async () => { - const { data } = await mutate({ mutation: markAsReadMutation, variables }) + const { data } = await mutate({ mutation: markAsReadMutation(), variables }) expect(data).toEqual({ markAsRead: { from: { @@ -401,4 +372,46 @@ describe('given some notifications', () => { }) }) }) + + describe('markAllAsRead', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const result = await mutate({ + mutation: markAllAsReadMutation(), + }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('not being notified at all', () => { + beforeEach(async () => { + variables = { + ...variables, + } + }) + + it('returns all as read', async () => { + const response = await mutate({ mutation: markAllAsReadMutation(), variables }) + expect(response.data.markAllAsRead).toEqual([ + { + createdAt: '2019-08-30T19:33:48.651Z', + from: { __typename: 'Comment', content: 'You have been mentioned in a comment' }, + read: true, + }, + { + createdAt: '2019-08-31T17:33:48.651Z', + from: { __typename: 'Post', content: 'You have been mentioned in a post' }, + read: true, + }, + ]) + expect(response.errors).toBeUndefined() + }) + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 1eaf7a708..86a278207 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -1524,9 +1524,9 @@ describe('Posts in Groups', () => { }) }) - it('does not show the posts of the closed group anymore', async () => { + it('stil shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1540,6 +1540,11 @@ describe('Posts in Groups', () => { title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', @@ -1564,9 +1569,9 @@ describe('Posts in Groups', () => { }) }) - it('does only show the public posts', async () => { + it('still shows the post of the hidden group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(2) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1580,6 +1585,16 @@ describe('Posts in Groups', () => { title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, ]), }, errors: undefined, @@ -1603,9 +1618,9 @@ describe('Posts in Groups', () => { authenticatedUser = await allGroupsUser.toJson() }) - it('does not show the posts of the closed group', async () => { + it('shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1624,6 +1639,11 @@ describe('Posts in Groups', () => { title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, ]), }, errors: undefined, diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index ce90fad1d..acf585f71 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -132,4 +132,9 @@ type Mutation { userId: ID! roleInGroup: GroupMemberRole! ): User + + RemoveUserFromGroup( + groupId: ID! + userId: ID! + ): User } diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 88ecd3882..864cdea4d 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -29,6 +29,7 @@ type Query { type Mutation { markAsRead(id: ID!): NOTIFIED + markAllAsRead: [NOTIFIED] } type Subscription { diff --git a/cypress/cypress.json b/cypress/cypress.json index dbe8691fa..de323f736 100644 --- a/cypress/cypress.json +++ b/cypress/cypress.json @@ -4,6 +4,7 @@ "ignoreTestFiles": "*.js", "chromeWebSecurity": false, "baseUrl": "http://localhost:3000", + "video":false, "retries": { "runMode": 2, "openMode": 0 diff --git a/deployment/DOCKER_MORE_CLOSELY.md b/deployment/DOCKER_MORE_CLOSELY.md new file mode 100644 index 000000000..67488fe81 --- /dev/null +++ b/deployment/DOCKER_MORE_CLOSELY.md @@ -0,0 +1,33 @@ +# Docker + +## Apple M1 Platform + +***Attention:** For using Docker commands in Apple M1 environments!* + +```bash +# set env variable for your shell +$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` + +For even more informations, see [Docker More Closely](#docker-more-closely) + +### Docker Compose Override File For Apple M1 Platform + +For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: + +```bash +# in main folder + +# for production +$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up + +# for production testing Docker images from DockerHub +$ docker compose -f docker-compose.ocelotsocial-branded.yml -f docker-compose.apple-m1.override.yml up + +# only once: init admin user and create indexes and constraints in Neo4j database +$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" +``` + +## Docker More Closely In Main Code + +To get more informations about the Apple M1 platform and to analyze the Docker builds etc. you find our documentation in our main code, [here](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/DOCKER_MORE_CLOSELY.md). diff --git a/neo4j/README.md b/neo4j/README.md index 885f7f445..1ea625d89 100644 --- a/neo4j/README.md +++ b/neo4j/README.md @@ -44,7 +44,7 @@ for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/), -on Archlinux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) +on Arch linux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). Just be sure to update the Neo4j connection string and credentials accordingly in `backend/.env`. @@ -55,7 +55,7 @@ Start Neo4J and confirm the database is running at [http://localhost:7474](http: Here we describe some rarely used Cypher commands for Neo4j that are needed from time to time: -### Index And Contraint Commands +### Index And Constraint Commands If indexes or constraints are missing or not set correctly, the browser search will not work or the database seed for development will not work. diff --git a/webapp/components/Category/index.vue b/webapp/components/Category/index.vue index 8e4014086..ebbaae584 100644 --- a/webapp/components/Category/index.vue +++ b/webapp/components/Category/index.vue @@ -30,6 +30,7 @@ export default { } } .filterActive { - background-color: $color-success-active; + color: $color-primary-inverse; + background-color: $color-primary-active; } diff --git a/webapp/components/FilterMenu/CategoriesMenu.vue b/webapp/components/FilterMenu/CategoriesMenu.vue index 0b5505503..091aed24f 100644 --- a/webapp/components/FilterMenu/CategoriesMenu.vue +++ b/webapp/components/FilterMenu/CategoriesMenu.vue @@ -3,7 +3,7 @@ {{ $t('admin.categories.name') }} -