Merge branch 'master' into unread-rooms-query

This commit is contained in:
Ulf Gebhardt 2023-07-17 10:23:56 +02:00
commit 1e77e9aec0
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
7 changed files with 424 additions and 90 deletions

View File

@ -2,8 +2,58 @@ name: ocelot.social end-to-end test CI
on: push
jobs:
docker_preparation:
name: Fullstack test preparation
runs-on: ubuntu-latest
outputs:
pr-number: ${{ steps.pr.outputs.number }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: Build docker images
run: |
mkdir /tmp/images
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar
- name: Install cypress requirements
run: |
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
cd backend
yarn install
yarn build
cd ..
yarn install
- name: Get pr number
id: pr
uses: 8BitJonny/gh-get-current-pr@2.2.0
- name: Cache docker images
id: cache
uses: actions/cache/save@v3.3.1
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ steps.pr.outputs.number }}
fullstack_tests:
name: Fullstack tests
if: success()
needs: docker_preparation
runs-on: ubuntu-latest
env:
jobs: 8
@ -12,28 +62,27 @@ jobs:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Restore cache
uses: actions/cache/restore@v3.3.1
id: cache
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}
fail-on-cache-miss: true
- name: webapp | copy env file
run: cp webapp/.env.template webapp/.env
- name: backend | copy env file
run: cp backend/.env.template backend/.env
- name: boot up test system | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: Full stack tests | prepare
- name: Boot up test system | docker-compose
run: |
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
chmod +x /opt/cucumber-json-formatter
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
cd backend
yarn install
yarn build
cd ..
yarn install
docker load < /tmp/images/neo4j.tar
docker load < /tmp/images/backend.tar
docker load < /tmp/images/webapp.tar
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
sleep 90s
- name: Full stack tests | run tests
id: e2e-tests
@ -44,17 +93,25 @@ jobs:
run: |
cd cypress/
node create-cucumber-html-report.js
- name: End-to-end tests | if tests failed, get pr number
id: pr
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: 8BitJonny/gh-get-current-pr@2.2.0
- name: End-to-end tests | if tests failed, upload report
- name: Full stack tests | if tests failed, upload report
id: e2e-report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: ocelot-e2e-test-report-pr${{ steps.pr.outputs.number }}
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
cleanup:
name: Cleanup
if: always()
needs: [docker_preparation, fullstack_tests]
runs-on: ubuntu-latest
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

View File

@ -11,6 +11,8 @@ import {
changeGroupMemberRoleMutation,
} from '../graphql/groups'
import { createPostMutation } from '../graphql/posts'
import { createRoomMutation } from '../graphql/rooms'
import { createMessageMutation } from '../graphql/messages'
import { createCommentMutation } from '../graphql/comments'
import { categories } from '../constants/categories'
@ -1553,6 +1555,90 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
)
await Factory.build('donations')
// Chat
authenticatedUser = await huey.toJson()
const { data: roomHueyPeter } = await mutate({
mutation: createRoomMutation(),
variables: {
userId: (await peterLustig.toJson()).id,
},
})
for (let i = 0; i < 30; i++) {
authenticatedUser = await huey.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: roomHueyPeter?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
authenticatedUser = await peterLustig.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: roomHueyPeter?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
}
authenticatedUser = await huey.toJson()
const { data: roomHueyJenny } = await mutate({
mutation: createRoomMutation(),
variables: {
userId: (await jennyRostock.toJson()).id,
},
})
for (let i = 0; i < 1000; i++) {
authenticatedUser = await huey.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: roomHueyJenny?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
authenticatedUser = await jennyRostock.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: roomHueyJenny?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
}
for (const user of additionalUsers) {
authenticatedUser = await jennyRostock.toJson()
const { data: room } = await mutate({
mutation: createRoomMutation(),
variables: {
userId: (await user.toJson()).id,
},
})
for (let i = 0; i < 29; i++) {
authenticatedUser = await jennyRostock.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: room?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
authenticatedUser = await user.toJson()
await mutate({
mutation: createMessageMutation(),
variables: {
roomId: room?.CreateRoom.id,
content: faker.lorem.sentence(),
},
})
}
}
/* eslint-disable-next-line no-console */
console.log('Seeded Data...')
await driver.close()

View File

@ -13,8 +13,8 @@ export const createRoomMutation = () => {
export const roomQuery = () => {
return gql`
query {
Room {
query Room($first: Int, $offset: Int, $id: ID) {
Room(first: $first, offset: $offset, id: $id, orderBy: createdAt_desc) {
id
roomId
roomName

View File

@ -51,6 +51,14 @@ describe('Room', () => {
id: 'not-chatting-user',
name: 'Not Chatting User',
}),
Factory.build('user', {
id: 'second-chatting-user',
name: 'Second Chatting User',
}),
Factory.build('user', {
id: 'third-chatting-user',
name: 'Third Chatting User',
}),
])
})
@ -366,6 +374,180 @@ describe('Room', () => {
).resolves.toMatchObject({
data: {
UnreadRooms: 2,
}
})
})
})
})
})
describe('query several rooms', () => {
beforeAll(async () => {
authenticatedUser = await chattingUser.toJson()
await mutate({
mutation: createRoomMutation(),
variables: {
userId: 'second-chatting-user',
},
})
await mutate({
mutation: createRoomMutation(),
variables: {
userId: 'third-chatting-user',
},
})
})
it('returns the rooms paginated', async () => {
expect(await query({ query: roomQuery(), variables: { first: 2, offset: 0 } })).toMatchObject(
{
errors: undefined,
data: {
Room: [
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Third Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'third-chatting-user',
id: 'third-chatting-user',
name: 'Third Chatting User',
avatar: {
url: expect.any(String),
},
},
]),
},
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Second Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'second-chatting-user',
id: 'second-chatting-user',
name: 'Second Chatting User',
avatar: {
url: expect.any(String),
},
},
]),
},
],
},
},
)
expect(await query({ query: roomQuery(), variables: { first: 2, offset: 2 } })).toMatchObject(
{
errors: undefined,
data: {
Room: [
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Other Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'other-chatting-user',
id: 'other-chatting-user',
name: 'Other Chatting User',
avatar: {
url: expect.any(String),
},
},
]),
},
],
},
},
)
})
})
describe('query single room', () => {
let result: any = null
beforeAll(async () => {
authenticatedUser = await chattingUser.toJson()
result = await query({ query: roomQuery() })
})
describe('as chatter of room', () => {
it('returns the room', async () => {
expect(
await query({
query: roomQuery(),
variables: { first: 2, offset: 0, id: result.data.Room[0].id },
}),
).toMatchObject({
errors: undefined,
data: {
Room: [
{
id: expect.any(String),
roomId: expect.any(String),
roomName: 'Third Chatting User',
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'third-chatting-user',
id: 'third-chatting-user',
name: 'Third Chatting User',
avatar: {
url: expect.any(String),
},
},
]),
},
],
},
})
})
describe('as not chatter of room', () => {
beforeAll(async () => {
authenticatedUser = await notChattingUser.toJson()
})
it('returns no room', async () => {
authenticatedUser = await notChattingUser.toJson()
expect(
await query({
query: roomQuery(),
variables: { first: 2, offset: 0, id: result.data.Room[0].id },
}),
).toMatchObject({
errors: undefined,
data: {
Room: [],
},
})
})

View File

@ -5,6 +5,11 @@
# users_some: _UserFilter
# }
# TODO change this to last message date
enum _RoomOrdering {
createdAt_desc
}
type Room {
id: ID!
createdAt: String
@ -24,6 +29,9 @@ type Mutation {
}
type Query {
Room: [Room]
Room(
id: ID
orderBy: [_RoomOrdering]
): [Room]
UnreadRooms: Int
}

View File

@ -13,13 +13,15 @@
:messages-loaded="messagesLoaded"
:rooms="JSON.stringify(rooms)"
:room-actions="JSON.stringify(roomActions)"
:rooms-loaded="true"
:rooms-loaded="roomsLoaded"
:loading-rooms="loadingRooms"
show-files="false"
show-audio="false"
:styles="JSON.stringify(computedChatStyle)"
:show-footer="true"
@send-message="sendMessage($event.detail[0])"
@fetch-messages="fetchMessages($event.detail[0])"
@fetch-more-rooms="fetchRooms"
:responsive-breakpoint="responsiveBreakpoint"
:single-room="singleRoom"
show-reaction-emojis="false"
@ -143,17 +145,20 @@ export default {
{ name: 'deleteRoom', title: 'Delete Room' },
*/
],
rooms: [],
messages: [],
messagesLoaded: true,
showDemoOptions: true,
responsiveBreakpoint: 600,
rooms: [],
roomsLoaded: false,
roomPage: 0,
roomPageSize: 10, // TODO pagination is a problem with single rooms - cant use
singleRoom: !!this.singleRoomId || false,
selectedRoom: null,
loadingRooms: true,
messagesLoaded: false,
messagePage: 0,
messagePageSize: 20,
roomPage: 0,
roomPageSize: 999, // TODO pagination is a problem with single rooms - cant use
selectedRoom: null,
messages: [],
}
},
mounted() {
@ -165,8 +170,8 @@ export default {
userId: this.singleRoomId,
},
})
.then(() => {
this.$apollo.queries.Rooms.refetch()
.then(({ data: { CreateRoom } }) => {
this.fetchRooms({ room: CreateRoom })
})
.catch((error) => {
this.$toast.error(error)
@ -174,6 +179,8 @@ export default {
.finally(() => {
// this.loading = false
})
} else {
this.fetchRooms()
}
},
computed: {
@ -181,17 +188,54 @@ export default {
currentUser: 'auth/user',
}),
computedChatStyle() {
// TODO light/dark theme still needed?
// return this.theme === 'light' ? chatStyle.STYLE.light : chatStyle.STYLE.dark
return chatStyle.STYLE.light
},
},
methods: {
async fetchRooms({ room } = {}) {
this.roomsLoaded = false
const offset = this.roomPage * this.roomPageSize
try {
const {
data: { Room },
} = await this.$apollo.query({
query: roomQuery(),
variables: {
id: room?.id,
first: this.roomPageSize,
offset,
},
fetchPolicy: 'no-cache',
})
const newRooms = Room.map((r) => {
return {
...r,
users: r.users.map((u) => {
return { ...u, username: u.name, avatar: u.avatar?.url }
}),
}
})
this.rooms = [...this.rooms, ...newRooms]
if (Room.length < this.roomPageSize) {
this.roomsLoaded = true
}
this.roomPage += 1
} catch (error) {
this.rooms = []
this.$toast.error(error.message)
}
// must be set false after initial rooms are loaded and never changed again
this.loadingRooms = false
},
async fetchMessages({ room, options = {} }) {
if (this.selectedRoom !== room.id) {
if (this.selectedRoom?.id !== room.id) {
this.messages = []
this.messagePage = 0
this.selectedRoom = room.id
this.selectedRoom = room
}
this.messagesLoaded = options.refetch ? this.messagesLoaded : false
const offset = (options.refetch ? 0 : this.messagePage) * this.messagePageSize
@ -224,13 +268,6 @@ export default {
}
},
refetchMessage(roomId) {
this.fetchMessages({
room: this.rooms.find((r) => r.roomId === roomId),
options: { refetch: true },
})
},
async sendMessage(message) {
try {
await this.$apollo.mutate({
@ -243,7 +280,10 @@ export default {
} catch (error) {
this.$toast.error(error.message)
}
this.refetchMessage(message.roomId)
this.fetchMessages({
room: this.rooms.find((r) => r.roomId === message.roomId),
options: { refetch: true },
})
},
getInitialsName(fullname) {
@ -251,45 +291,6 @@ export default {
return fullname.match(/\b\w/g).join('').substring(0, 3).toUpperCase()
},
},
apollo: {
Rooms: {
query() {
return roomQuery()
},
variables() {
return {
first: this.roomPageSize,
offset: this.roomPage * this.roomPageSize,
}
},
update({ Room }) {
if (!Room) {
this.rooms = []
return
}
// Backend result needs mapping of the following values
// room[i].users[j].name -> room[i].users[j].username
// room[i].users[j].avatar.url -> room[i].users[j].avatar
// also filter rooms for the single room
this.rooms = Room.map((r) => {
return {
...r,
users: r.users.map((u) => {
return { ...u, username: u.name, avatar: u.avatar?.url }
}),
}
}).filter((r) =>
this.singleRoom ? r.users.filter((u) => u.id === this.singleRoomId).length > 0 : true,
)
},
error(error) {
this.rooms = []
this.$toast.error(error.message)
},
fetchPolicy: 'no-cache',
},
},
}
</script>
<style lang="scss">

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'
export const roomQuery = () => gql`
query Room($first: Int, $offset: Int) {
Room(first: $first, offset: $offset) {
query Room($first: Int, $offset: Int, $id: ID) {
Room(first: $first, offset: $offset, id: $id, orderBy: createdAt_desc) {
id
roomId
roomName