Skip to content

Commit

Permalink
channel api tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tudddorrr committed Dec 11, 2024
1 parent a51acbf commit 7715cec
Show file tree
Hide file tree
Showing 17 changed files with 1,155 additions and 50 deletions.
47 changes: 45 additions & 2 deletions src/docs/game-channel-api.docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,47 @@ const GameChannelAPIDocs: APIDocs<GameChannelAPIService> = {
}
]
},
subscriptions: {
description: 'List game channels that the player is subscribed to',
params: {
headers: {
'x-talo-alias': 'The ID of the player\'s alias'
}
},
samples: [
{
title: 'Sample response',
sample: {
channels: [
{
id: 1,
name: 'general-chat',
owner: null,
totalMessages: 308,
memberCount: 42,
props: [
{ key: 'channelType', value: 'public' }
],
createdAt: '2024-12-09T12:00:00.000Z',
updatedAt: '2024-12-09T12:00:00.000Z'
},
{
id: 2,
name: 'trade-chat',
owner: null,
totalMessages: 23439,
memberCount: 124,
props: [
{ key: 'channelType', value: 'public' }
],
createdAt: '2024-12-09T12:00:00.000Z',
updatedAt: '2024-12-09T12:00:00.000Z'
}
]
}
}
]
},
post: {
description: 'Create a game channel',
params: {
Expand All @@ -67,7 +108,8 @@ const GameChannelAPIDocs: APIDocs<GameChannelAPIService> = {
},
body: {
name: 'The name of the channel',
props: 'An array of @type(Props:prop)'
props: 'An array of @type(Props:prop)',
autoCleanup: 'Whether the channel should be automatically deleted when the owner leaves or the channel is empty (default is false)'
}
},
samples: [
Expand All @@ -78,7 +120,8 @@ const GameChannelAPIDocs: APIDocs<GameChannelAPIService> = {
props: [
{ key: 'channelType', value: 'guild' },
{ key: 'guildId', value: '5912' }
]
],
autoCleanup: true
}
},
{
Expand Down
11 changes: 10 additions & 1 deletion src/entities/game-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class GameChannel {
id: number

@Required({
methods: ['POST'],
validation: async (val: unknown, req: Request): Promise<ValidationCondition[]> => {
const duplicateName = await (<EntityManager>req.ctx.em).getRepository(GameChannel).findOne({
name: val,
Expand All @@ -38,13 +39,21 @@ export default class GameChannel {
@Property()
totalMessages: number = 0

@Required()
@Property()
autoCleanup: boolean = false

@ManyToOne(() => Game)
game: Game

@Required({
methods: [],
validation: async (val: unknown): Promise<ValidationCondition[]> => [
{
check: Array.isArray(val),
error: 'Props must be an array'
}
]
})
@Embedded(() => Prop, { array: true })
props: Prop[] = []

Expand Down
62 changes: 27 additions & 35 deletions src/services/api/game-channel-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import GameChannel from '../../entities/game-channel'
import { EntityManager } from '@mikro-orm/mysql'
import sanitiseProps from '../../lib/props/sanitiseProps'
import Socket from '../../socket'
import { sendMessage, sendMessages, SocketMessageResponse } from '../../socket/messages/socketMessage'
import { sendMessages, SocketMessageResponse } from '../../socket/messages/socketMessage'
import GameChannelAPIDocs from '../../docs/game-channel-api.docs'
import PlayerAlias from '../../entities/player-alias'
import { uniqWith } from 'lodash'

function sendMessageToChannelMembers<T>(req: Request, channel: GameChannel, res: SocketMessageResponse, data: T) {
const socket: Socket = req.ctx.wss
Expand All @@ -16,7 +17,7 @@ function sendMessageToChannelMembers<T>(req: Request, channel: GameChannel, res:
}

function canModifyChannel(channel: GameChannel, alias: PlayerAlias): boolean {
return channel.owner ? channel.owner.id === alias.id : true
return channel.owner ? channel.owner.id === alias.id : false
}

@Routes([
Expand Down Expand Up @@ -90,25 +91,22 @@ export default class GameChannelAPIService extends APIService {
})
@HasPermission(GameChannelAPIPolicy, 'post')
async post(req: Request): Promise<Response> {
const { name, props } = req.body
const { name, props, autoCleanup } = req.body
const em: EntityManager = req.ctx.em

const channel = new GameChannel(req.ctx.state.game)
channel.name = name
channel.owner = req.ctx.state.alias
channel.members.add(req.ctx.state.alias)
channel.autoCleanup = autoCleanup ?? false

if (props) {
channel.props = sanitiseProps(props)
}

await em.persistAndFlush(channel)

const socket: Socket = req.ctx.wss
const conn = socket.findConnections((conn) => conn.playerAliasId === req.ctx.state.alias.id)[0]
if (conn) {
sendMessage(conn, 'v1.channels.player-joined', { channel })
}
sendMessageToChannelMembers(req, channel, 'v1.channels.player-joined', { channel })

return {
status: 200,
Expand All @@ -127,12 +125,11 @@ export default class GameChannelAPIService extends APIService {
const channel: GameChannel = req.ctx.state.channel

if (!(await channel.members.load()).getIdentifiers().includes(req.ctx.state.alias.id)) {
sendMessageToChannelMembers(req, channel, 'v1.channels.player-joined', { channel })
channel.members.add(req.ctx.state.alias)
await em.flush()
}

sendMessageToChannelMembers(req, channel, 'v1.channels.player-joined', { channel })

return {
status: 200,
body: {
Expand All @@ -149,18 +146,23 @@ export default class GameChannelAPIService extends APIService {
const em: EntityManager = req.ctx.em
const channel: GameChannel = req.ctx.state.channel

if (channel.autoCleanup && channel.owner.id === req.ctx.state.alias.id) {
if (channel.autoCleanup && (channel.owner.id === req.ctx.state.alias.id || await channel.members.loadCount() === 1)) {
await em.removeAndFlush(channel)

return {
status: 204
}
}

(await channel.members.load()).remove(req.ctx.state.alias)
await em.flush()
if ((await channel.members.load()).getIdentifiers().includes(req.ctx.state.alias.id)) {
if (channel.owner.id === req.ctx.state.alias.id) {
channel.owner = null
}

sendMessageToChannelMembers(req, channel, 'v1.channels.player-left', { channel })
channel.members.remove(req.ctx.state.alias)
sendMessageToChannelMembers(req, channel, 'v1.channels.player-left', { channel })
await em.flush()
}

return {
status: 204
Expand All @@ -177,21 +179,21 @@ export default class GameChannelAPIService extends APIService {
const em: EntityManager = req.ctx.em
const channel: GameChannel = req.ctx.state.channel

if (canModifyChannel(channel, req.ctx.state.alias)) {
return {
status: 403,
body: {
error: 'This player is not the owner of the channel'
}
}
if (!canModifyChannel(channel, req.ctx.state.alias)) {
req.ctx.throw(403, 'This player is not the owner of the channel')
}

if (name) {
channel.name = name
}

if (props) {
channel.props = sanitiseProps(props)
const mergedProps = uniqWith([
...sanitiseProps(props),
...channel.props
], (a, b) => a.key === b.key)

channel.props = sanitiseProps(mergedProps, true)
}

if (ownerAliasId) {
Expand All @@ -203,12 +205,7 @@ export default class GameChannelAPIService extends APIService {
})

if (!newOwner) {
return {
status: 404,
body: {
error: 'Owner alias not found'
}
}
req.ctx.throw(404, 'New owner not found')
}

channel.owner = newOwner
Expand All @@ -232,13 +229,8 @@ export default class GameChannelAPIService extends APIService {
const em: EntityManager = req.ctx.em
const channel: GameChannel = req.ctx.state.channel

if (canModifyChannel(channel, req.ctx.state.alias)) {
return {
status: 403,
body: {
error: 'This player is not the owner of the channel'
}
}
if (!canModifyChannel(channel, req.ctx.state.alias)) {
req.ctx.throw(403, 'This player is not the owner of the channel')
}

await channel.members.removeAll()
Expand Down
6 changes: 6 additions & 0 deletions src/socket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ export default class Socket {
heartbeat(): void {
const interval = setInterval(() => {
this.connections.forEach((conn) => {
/* v8 ignore start */
if (!conn.alive) {
conn.ws.terminate()
return
}

conn.alive = false
conn.ws.ping()
/* v8 ignore end */
})
}, 30_000)

Expand All @@ -66,23 +68,27 @@ export default class Socket {
})
}

/* v8 ignore start */
handlePong(ws: WebSocket): void {
const connection = this.findConnectionBySocket(ws)
if (!connection) return

connection.alive = true
}
/* v8 ignore end */

handleCloseConnection(ws: WebSocket): void {
this.connections = this.connections.filter((conn) => conn.ws !== ws)
}

findConnectionBySocket(ws: WebSocket): SocketConnection | undefined {
const connection = this.connections.find((conn) => conn.ws === ws)
/* v8 ignore start */
if (!connection) {
ws.close(3000)
return
}
/* v8 ignore end */

return connection
}
Expand Down
2 changes: 1 addition & 1 deletion src/socket/router/socketRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class SocketRouter {
if (err instanceof ZodError) {
sendError(conn, message.req, new SocketError('INVALID_MESSAGE', 'Invalid message data for request'))
} else {
sendError(conn, message?.req, new SocketError('LISTENER_ERROR', 'An error occurred while processing the message', err.message))
sendError(conn, message?.req ?? 'unknown', new SocketError('LISTENER_ERROR', 'An error occurred while processing the message', err.message))
}
}
}
Expand Down
Loading

0 comments on commit 7715cec

Please sign in to comment.