Merge pull request 'Restructure rooms codebase and write tests' (#21) from structure-handlers into main
Reviewed-on: https://git.verdigado.com/NB-Public/rocketchat2matrix/pulls/21
This commit is contained in:
commit
4c5d4ce72e
332
package-lock.json
generated
332
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.2",
|
||||
"@jest/globals": "^29.5.0",
|
||||
"@types/n-readlines": "^1.0.3",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
import { expect, jest, test } from '@jest/globals'
|
||||
import axios from 'axios'
|
||||
import { IdMapping } from '../entity/IdMapping'
|
||||
import * as storage from '../helpers/storage'
|
||||
import {
|
||||
MatrixRoomPresets,
|
||||
MatrixRoomVisibility,
|
||||
RcRoomTypes,
|
||||
acceptInvitation,
|
||||
getCreator,
|
||||
getFilteredMembers,
|
||||
inviteMember,
|
||||
mapRoom,
|
||||
parseMemberships,
|
||||
registerRoom,
|
||||
} from './rooms'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
jest.mock('../helpers/storage')
|
||||
const mockedStorage = storage as jest.Mocked<typeof storage>
|
||||
|
||||
const roomCreator = {
|
||||
_id: 'roomcreatorid',
|
||||
name: 'RoomCreator',
|
||||
@ -13,35 +29,51 @@ const roomCreator = {
|
||||
__rooms: [],
|
||||
}
|
||||
|
||||
test('mapping direct chats', () => {
|
||||
expect(
|
||||
mapRoom({
|
||||
const rcDirectChat = {
|
||||
_id: 'aliceidbobid',
|
||||
t: RcRoomTypes.direct,
|
||||
usernames: ['Alice', 'Bob'],
|
||||
uids: ['aliceid', 'bobid'],
|
||||
})
|
||||
).toEqual({
|
||||
is_direct: true,
|
||||
preset: MatrixRoomPresets.trusted,
|
||||
creation_content: {
|
||||
'm.federate': false,
|
||||
},
|
||||
_creatorId: 'aliceid',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('mapping public rooms', () => {
|
||||
expect(
|
||||
mapRoom({
|
||||
const rcPublicRoom = {
|
||||
_id: 'randomRoomId',
|
||||
fname: 'public',
|
||||
description: 'Public chat room',
|
||||
name: 'public',
|
||||
t: RcRoomTypes.chat,
|
||||
u: roomCreator,
|
||||
}
|
||||
|
||||
const rcPrivateRoom = {
|
||||
_id: 'privateRoomId',
|
||||
name: 'private',
|
||||
fname: 'private',
|
||||
description: 'Private chat room',
|
||||
t: RcRoomTypes.private,
|
||||
u: roomCreator,
|
||||
}
|
||||
|
||||
const sessionOption = {
|
||||
headers: { Authorization: 'Bearer secretAuthToken' },
|
||||
testingOption: 'there might be other options',
|
||||
}
|
||||
|
||||
const room_id = '!randomId:my.matrix.host'
|
||||
|
||||
test('mapping direct chats', () => {
|
||||
expect(mapRoom(rcDirectChat)).toEqual({
|
||||
is_direct: true,
|
||||
preset: MatrixRoomPresets.trusted,
|
||||
creation_content: {
|
||||
'm.federate': false,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
expect(getCreator(rcDirectChat)).toBe('aliceid')
|
||||
})
|
||||
|
||||
test('mapping public rooms', () => {
|
||||
expect(mapRoom(rcPublicRoom)).toEqual({
|
||||
preset: MatrixRoomPresets.public,
|
||||
room_alias_name: 'public',
|
||||
name: 'public',
|
||||
@ -50,21 +82,12 @@ test('mapping public rooms', () => {
|
||||
'm.federate': false,
|
||||
},
|
||||
visibility: MatrixRoomVisibility.public,
|
||||
_creatorId: roomCreator._id,
|
||||
})
|
||||
expect(getCreator(rcPublicRoom)).toBe(roomCreator._id)
|
||||
})
|
||||
|
||||
test('mapping private rooms', () => {
|
||||
expect(
|
||||
mapRoom({
|
||||
_id: 'privateRoomId',
|
||||
name: 'private',
|
||||
fname: 'private',
|
||||
description: 'Private chat room',
|
||||
t: RcRoomTypes.private,
|
||||
u: roomCreator,
|
||||
})
|
||||
).toEqual({
|
||||
expect(mapRoom(rcPrivateRoom)).toEqual({
|
||||
preset: MatrixRoomPresets.private,
|
||||
room_alias_name: 'private',
|
||||
name: 'private',
|
||||
@ -72,8 +95,8 @@ test('mapping private rooms', () => {
|
||||
creation_content: {
|
||||
'm.federate': false,
|
||||
},
|
||||
_creatorId: roomCreator._id,
|
||||
})
|
||||
expect(getCreator(rcPrivateRoom)).toBe(roomCreator._id)
|
||||
})
|
||||
|
||||
test('mapping live chats', () => {
|
||||
@ -81,3 +104,108 @@ test('mapping live chats', () => {
|
||||
mapRoom({ _id: 'liveChatId', t: RcRoomTypes.live })
|
||||
).toThrowError('Room type l is unknown')
|
||||
})
|
||||
|
||||
test('getting creator', () => {
|
||||
expect(getCreator(rcDirectChat)).toBe('aliceid')
|
||||
expect(getCreator(rcPublicRoom)).toBe(roomCreator._id)
|
||||
expect(getCreator(rcPrivateRoom)).toBe(roomCreator._id)
|
||||
})
|
||||
|
||||
test('creating memberships for direct chats', async () => {
|
||||
await expect(parseMemberships(rcDirectChat)).resolves.toBe(undefined)
|
||||
expect(mockedStorage.createMembership).toHaveBeenCalledWith(
|
||||
rcDirectChat._id,
|
||||
rcDirectChat.uids[0]
|
||||
)
|
||||
expect(mockedStorage.createMembership).toHaveBeenCalledWith(
|
||||
rcDirectChat._id,
|
||||
rcDirectChat.uids[1]
|
||||
)
|
||||
expect(mockedStorage.createMembership).toHaveBeenCalledTimes(2)
|
||||
|
||||
mockedStorage.createMembership.mockClear()
|
||||
|
||||
await expect(
|
||||
parseMemberships({ ...rcDirectChat, _id: 'hoihoi', uids: ['hoi', 'hoi'] })
|
||||
).resolves.toBe(undefined)
|
||||
|
||||
expect(mockedStorage.createMembership).toHaveBeenCalledWith('hoihoi', 'hoi')
|
||||
expect(mockedStorage.createMembership).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('registering room', async () => {
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: { room_id },
|
||||
})
|
||||
expect(await registerRoom(rcPublicRoom, sessionOption)).toBe(room_id)
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/_matrix/client/v3/createRoom',
|
||||
rcPublicRoom,
|
||||
sessionOption
|
||||
)
|
||||
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
||||
mockedAxios.post.mockClear()
|
||||
})
|
||||
|
||||
test('inviting member', async () => {
|
||||
await expect(
|
||||
inviteMember('inviteme', room_id, sessionOption)
|
||||
).resolves.not.toThrow()
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/_matrix/client/v3/rooms/${room_id}/invite`,
|
||||
{ user_id: 'inviteme' },
|
||||
sessionOption
|
||||
)
|
||||
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
||||
mockedAxios.post.mockClear()
|
||||
})
|
||||
|
||||
test('accepting invitation by joining the room', async () => {
|
||||
await expect(
|
||||
acceptInvitation(
|
||||
{
|
||||
rcId: 'whatever',
|
||||
matrixId: 'Neo',
|
||||
accessToken: 'secretAuthToken',
|
||||
type: 0,
|
||||
},
|
||||
room_id
|
||||
)
|
||||
).resolves.toBe(undefined)
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/_matrix/client/v3/join/${room_id}`,
|
||||
{},
|
||||
{ headers: sessionOption.headers }
|
||||
)
|
||||
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
|
||||
mockedAxios.post.mockClear()
|
||||
})
|
||||
|
||||
test('filtering members', async () => {
|
||||
const members = [
|
||||
'creator',
|
||||
'existingUser',
|
||||
'otherExistingUser',
|
||||
'excludedUser',
|
||||
]
|
||||
function mockMapping(rcId: string, type?: number): IdMapping {
|
||||
return {
|
||||
rcId,
|
||||
matrixId: `@${rcId}:matrix`,
|
||||
type: type || 0,
|
||||
accessToken: 'accessToken',
|
||||
}
|
||||
}
|
||||
|
||||
mockedStorage.getMapping.mockImplementation(async (rcId, type) =>
|
||||
rcId.includes('excluded') || !rcId ? null : mockMapping(rcId, type)
|
||||
)
|
||||
|
||||
await expect(getFilteredMembers(members, members[0])).resolves.toStrictEqual([
|
||||
mockMapping('existingUser'),
|
||||
mockMapping('otherExistingUser'),
|
||||
])
|
||||
expect(mockedStorage.getMapping).toBeCalledWith('existingUser', 0)
|
||||
expect(mockedStorage.getMapping).toBeCalledWith('otherExistingUser', 0)
|
||||
expect(mockedStorage.getMapping).toBeCalledWith('excludedUser', 0)
|
||||
})
|
||||
|
||||
@ -5,7 +5,11 @@ import {
|
||||
getMapping,
|
||||
getMemberships,
|
||||
} from '../helpers/storage'
|
||||
import { axios, getUserSessionOptions } from '../helpers/synapse'
|
||||
import {
|
||||
axios,
|
||||
formatUserSessionOptions,
|
||||
getUserSessionOptions,
|
||||
} from '../helpers/synapse'
|
||||
import { RcUser } from './users'
|
||||
|
||||
export const enum RcRoomTypes {
|
||||
@ -47,7 +51,6 @@ export type MatrixRoom = {
|
||||
is_direct?: boolean
|
||||
preset?: MatrixRoomPresets
|
||||
visibility?: MatrixRoomVisibility
|
||||
_creatorId?: string
|
||||
}
|
||||
|
||||
export function mapRoom(rcRoom: RcRoom): MatrixRoom {
|
||||
@ -55,7 +58,6 @@ export function mapRoom(rcRoom: RcRoom): MatrixRoom {
|
||||
creation_content: {
|
||||
'm.federate': false,
|
||||
},
|
||||
_creatorId: '',
|
||||
}
|
||||
rcRoom.name && (room.name = rcRoom.name)
|
||||
rcRoom.name && (room.room_alias_name = rcRoom.name)
|
||||
@ -65,18 +67,15 @@ export function mapRoom(rcRoom: RcRoom): MatrixRoom {
|
||||
case RcRoomTypes.direct:
|
||||
room.is_direct = true
|
||||
room.preset = MatrixRoomPresets.trusted
|
||||
room._creatorId = rcRoom.uids?.[0] || ''
|
||||
break
|
||||
|
||||
case RcRoomTypes.chat:
|
||||
room.preset = MatrixRoomPresets.public
|
||||
room.visibility = MatrixRoomVisibility.public
|
||||
room._creatorId = rcRoom.u?._id || ''
|
||||
break
|
||||
|
||||
case RcRoomTypes.private:
|
||||
room.preset = MatrixRoomPresets.private
|
||||
room._creatorId = rcRoom.u?._id || ''
|
||||
break
|
||||
|
||||
case RcRoomTypes.live:
|
||||
@ -85,15 +84,23 @@ export function mapRoom(rcRoom: RcRoom): MatrixRoom {
|
||||
log.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
if (!room._creatorId) {
|
||||
log.warn(
|
||||
`Creator ID could not be determined for room ${rcRoom.name} of type ${rcRoom.t}.`
|
||||
)
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
export async function parseMemberships(rcRoom: RcRoom) {
|
||||
export function getCreator(rcRoom: RcRoom): string {
|
||||
if (rcRoom.u && rcRoom.u._id) {
|
||||
return rcRoom.u._id
|
||||
} else if (rcRoom.uids && rcRoom.uids.length > 1) {
|
||||
return rcRoom.uids[0]
|
||||
} else {
|
||||
log.warn(
|
||||
`Creator ID could not be determined for room ${rcRoom.name} of type ${rcRoom.t}. This is normal for the default room.`
|
||||
)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMemberships(rcRoom: RcRoom): Promise<void> {
|
||||
if (rcRoom.t == RcRoomTypes.direct && rcRoom.uids) {
|
||||
await Promise.all(
|
||||
[...new Set(rcRoom.uids)] // Deduplicate users
|
||||
@ -105,62 +112,107 @@ export async function parseMemberships(rcRoom: RcRoom) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRoom(rcRoom: RcRoom): Promise<MatrixRoom> {
|
||||
const room: MatrixRoom = mapRoom(rcRoom)
|
||||
const creatorId = room._creatorId || ''
|
||||
delete room._creatorId
|
||||
await parseMemberships(rcRoom)
|
||||
let sessionOptions = {}
|
||||
if (room._creatorId) {
|
||||
export async function getCreatorSessionOptions(
|
||||
creatorId: string
|
||||
): Promise<object> {
|
||||
if (creatorId) {
|
||||
try {
|
||||
sessionOptions = await getUserSessionOptions(creatorId)
|
||||
log.debug('Room user session generated:', sessionOptions)
|
||||
const creatorSessionOptions = await getUserSessionOptions(creatorId)
|
||||
log.debug('Room owner session generated:', creatorSessionOptions)
|
||||
return creatorSessionOptions
|
||||
} catch (error) {
|
||||
log.warn(error)
|
||||
// TODO: Skip room, if it has 0-1 member or is a direct chat?
|
||||
}
|
||||
}
|
||||
log.debug('Creating room:', room)
|
||||
return {}
|
||||
}
|
||||
|
||||
room.room_id = (
|
||||
await axios.post('/_matrix/client/v3/createRoom', room, sessionOptions)
|
||||
export async function registerRoom(
|
||||
room: MatrixRoom,
|
||||
creatorSessionOptions: object
|
||||
): Promise<string> {
|
||||
return (
|
||||
await axios.post(
|
||||
'/_matrix/client/v3/createRoom',
|
||||
room,
|
||||
creatorSessionOptions
|
||||
)
|
||||
).data.room_id
|
||||
}
|
||||
|
||||
// TODO: Invite members and let them join
|
||||
const members = await getMemberships(rcRoom._id)
|
||||
log.info(`Inviting members to room ${rcRoom._id}:`, members)
|
||||
export async function inviteMember(
|
||||
inviteeId: string,
|
||||
roomId: string,
|
||||
creatorSessionOptions: object
|
||||
): Promise<void> {
|
||||
log.http(`Invite member ${inviteeId}`)
|
||||
await axios.post(
|
||||
`/_matrix/client/v3/rooms/${roomId}/invite`,
|
||||
{ user_id: inviteeId },
|
||||
creatorSessionOptions
|
||||
)
|
||||
}
|
||||
|
||||
export async function acceptInvitation(
|
||||
inviteeMapping: IdMapping,
|
||||
roomId: string
|
||||
): Promise<void> {
|
||||
log.http(
|
||||
`Accepting invitation for member ${inviteeMapping.rcId} aka. ${inviteeMapping.matrixId}`
|
||||
)
|
||||
await axios.post(
|
||||
`/_matrix/client/v3/join/${roomId}`,
|
||||
{},
|
||||
formatUserSessionOptions(inviteeMapping.accessToken || '')
|
||||
)
|
||||
}
|
||||
|
||||
export async function getFilteredMembers(
|
||||
rcMemberIds: string[],
|
||||
creatorId: string
|
||||
): Promise<IdMapping[]> {
|
||||
const memberMappings = (
|
||||
await Promise.all(
|
||||
members
|
||||
rcMemberIds
|
||||
.filter((rcMemberId) => rcMemberId != creatorId)
|
||||
.map(async (rcMemberId) => await getMapping(rcMemberId, 0))
|
||||
)
|
||||
)
|
||||
.filter((mapping): mapping is IdMapping => mapping != null)
|
||||
.map(async (mapping) => {
|
||||
log.http(`Invite member ${mapping.rcId} aka. ${mapping.matrixId}`)
|
||||
await axios.post(
|
||||
`/_matrix/client/v3/rooms/${room.room_id}/invite`,
|
||||
{ user_id: mapping.matrixId },
|
||||
sessionOptions
|
||||
)
|
||||
|
||||
log.http(
|
||||
`Accepting invitation for member ${mapping.rcId} aka. ${mapping.matrixId}`
|
||||
)
|
||||
await axios.post(
|
||||
`/_matrix/client/v3/join/${room.room_id}`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${mapping.accessToken}`,
|
||||
},
|
||||
).filter((memberMapping): memberMapping is IdMapping => memberMapping != null)
|
||||
return memberMappings
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(memberMappings)
|
||||
export async function createRoom(rcRoom: RcRoom): Promise<MatrixRoom> {
|
||||
const room: MatrixRoom = mapRoom(rcRoom)
|
||||
const creatorId = getCreator(rcRoom)
|
||||
await parseMemberships(rcRoom)
|
||||
const creatorSessionOptions = await getCreatorSessionOptions(creatorId)
|
||||
log.debug('Creating room:', room)
|
||||
|
||||
room.room_id = await registerRoom(room, creatorSessionOptions)
|
||||
|
||||
const rcMemberIds = await getMemberships(rcRoom._id)
|
||||
const memberMappings = await getFilteredMembers(rcMemberIds, creatorId)
|
||||
log.info(
|
||||
`Inviting members to room ${rcRoom._id}:`,
|
||||
memberMappings.map((mapping) => mapping.matrixId)
|
||||
)
|
||||
log.debug(
|
||||
'Excluded members:',
|
||||
rcMemberIds.filter(
|
||||
(x) => !memberMappings.map((mapping) => mapping.rcId).includes(x)
|
||||
)
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
memberMappings.map(async (memberMapping) => {
|
||||
await inviteMember(
|
||||
memberMapping.matrixId || '',
|
||||
room.room_id || '',
|
||||
creatorSessionOptions
|
||||
)
|
||||
await acceptInvitation(memberMapping, room.room_id || '')
|
||||
})
|
||||
)
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
27
src/handlers/roomsWithMockedSynapse.test.ts
Normal file
27
src/handlers/roomsWithMockedSynapse.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { getUserSessionOptions } from '../helpers/synapse'
|
||||
import { jest, expect, test } from '@jest/globals'
|
||||
import { getCreatorSessionOptions } from './rooms'
|
||||
|
||||
jest.mock('../helpers/synapse')
|
||||
const mockedGetUserSessionOptions = getUserSessionOptions as jest.Mocked<
|
||||
typeof getUserSessionOptions
|
||||
>
|
||||
|
||||
const sessionOption = {
|
||||
headers: { Authorization: 'Bearer secretAuthToken' },
|
||||
}
|
||||
|
||||
test('getting token for different room creators', async () => {
|
||||
mockedGetUserSessionOptions.mockImplementation(async (id: string) => {
|
||||
if (id.includes('excluded')) {
|
||||
throw new Error(`Could not retrieve access token for ID ${id}`)
|
||||
}
|
||||
return sessionOption
|
||||
})
|
||||
|
||||
expect(await getCreatorSessionOptions('')).toStrictEqual({})
|
||||
expect(await getCreatorSessionOptions('excludedUser')).toStrictEqual({})
|
||||
expect(await getCreatorSessionOptions('creator')).toStrictEqual(sessionOption)
|
||||
expect(mockedGetUserSessionOptions).toHaveBeenCalledWith('excludedUser')
|
||||
expect(mockedGetUserSessionOptions).toHaveBeenCalledWith('creator')
|
||||
})
|
||||
@ -1,4 +1,5 @@
|
||||
process.env.REGISTRATION_SHARED_SECRET = 'ThisIsSoSecretWow'
|
||||
import { expect, jest, test } from '@jest/globals'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
MatrixUser,
|
||||
@ -32,6 +33,7 @@ const nonce = 'test-nonce'
|
||||
test('mapping users', () => {
|
||||
expect(mapUser(rcUser)).toStrictEqual(matrixUser)
|
||||
})
|
||||
|
||||
test('generating correct hmac', () => {
|
||||
expect(generateHmac({ ...matrixUser, nonce })).toStrictEqual(
|
||||
'be0537407ab3c82de908c5763185556e98a7211c'
|
||||
|
||||
@ -22,10 +22,14 @@ export const whoami = () =>
|
||||
})
|
||||
})
|
||||
|
||||
export function formatUserSessionOptions(accessToken: string) {
|
||||
return { headers: { Authorization: `Bearer ${accessToken}` } }
|
||||
}
|
||||
|
||||
export async function getUserSessionOptions(id: string) {
|
||||
const accessToken = await getAccessToken(id)
|
||||
if (!accessToken) {
|
||||
throw new Error(`Could not retrieve access token for ID ${id}`)
|
||||
}
|
||||
return { headers: { Authorization: `Bearer ${accessToken}` } }
|
||||
return formatUserSessionOptions(accessToken)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user