Backend: Test channel delete with a custom testing library

This commit is contained in:
ADAMJR 2021-12-20 16:01:50 +00:00
parent a3561ef9b5
commit 1c4418ab58
21 changed files with 163 additions and 144 deletions

View File

@ -99,7 +99,7 @@
},
"node_modules/@accord/ion": {
"version": "1.0.3",
"resolved": "git+ssh://git@github.com/accord-dot-app/ion.git#590fcd2115f5b7b32bb3a0960a96e8ec21db2d13",
"resolved": "git+ssh://git@github.com/accord-dot-app/ion.git#ca90af119f61f503646978b381b17977f10bb90e",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
@ -6906,7 +6906,7 @@
"version": "file:src"
},
"@accord/ion": {
"version": "git+ssh://git@github.com/accord-dot-app/ion.git#590fcd2115f5b7b32bb3a0960a96e8ec21db2d13",
"version": "git+ssh://git@github.com/accord-dot-app/ion.git#ca90af119f61f503646978b381b17977f10bb90e",
"from": "@accord/ion@github:accord-dot-app/ion",
"requires": {
"@accord/ion": "github:accord-dot-app/ion",

View File

@ -7,7 +7,7 @@
"start:debug": "nodemon --exec 'node --inspect=0.0.0.0:9229 --require ts-node/register src/app.ts' --ext 'ts,yml'",
"start:prod": "ts-node-transpile-only src/app.ts",
"test": "npm run test:int test:unit",
"test:int": "ts-mocha --exit test/int/test.ts",
"test:int": "ts-mocha --exit test/e2e/test.ts",
"test:unit": "ts-mocha --exit test/unit/**/**.test.ts"
},
"keywords": [],
@ -43,7 +43,6 @@
"rate-limit-mongo": "^2.3.1",
"re2": "^1.16.0",
"socket.io": "^4.0.0",
"socket.io-client": "^4.0.0",
"striptags": "^3.2.0",
"typescript": "^4.2.3",
"winston": "^3.3.3"
@ -79,6 +78,7 @@
"chai-spies": "^1.0.0",
"mocha": "^8.2.1",
"nodemon": "^2.0.14",
"socket.io-client": "^4.0.0",
"supertest": "^6.1.3",
"ts-mocha": "^8.0.0",
"ts-node": "^10.4.0",

View File

@ -19,7 +19,7 @@ export default class Channels extends DBWrapper<string, ChannelDocument> {
public async create(options: Partial<Entity.Channel>): Promise<ChannelDocument> {
return Channel.create({
_id: generateSnowflake(),
_id: options.id ?? generateSnowflake(),
name: 'chat',
position: await Channel.countDocuments({ guildId: options.guildId }),
type: 'TEXT',

View File

@ -3,39 +3,36 @@ import DBWrapper from './db-wrapper';
import { GuildDocument } from './models/guild';
import { GuildMember, GuildMemberDocument } from './models/guild-member';
import { Role } from './models/role';
import { SelfUserDocument, UserDocument } from './models/user';
import { SelfUserDocument, User, UserDocument } from './models/user';
import { generateSnowflake } from './snowflake-entity';
export default class GuildMembers extends DBWrapper<string, GuildMemberDocument> {
public async get(id: string | undefined) {
const member = await GuildMember.findById(id);
if (!member)
throw new TypeError('Guild Member Not Found');
throw new TypeError('Guild member not found');
return member;
}
public async getInGuild(guildId: string | undefined, userId: string | undefined) {
const member = await GuildMember.findOne({ guildId, userId });
if (!member)
throw new TypeError('Guild Member Not Found');
throw new TypeError('Guild member not found');
return member;
}
public async create(guildId: string, user: SelfUserDocument) {
public async create(options: Partial<Entity.GuildMember>) {
const member = await GuildMember.create({
_id: generateSnowflake(),
guildId,
userId: user.id,
roleIds: [await this.getEveryoneRoleId(guildId)],
_id: options.id ?? generateSnowflake(),
roleIds: [await this.getEveryoneRoleId(options.guildId!)],
...options,
});
await this.addToUser(user, guildId);
await this.addGuildToUser(options.userId!, options.guildId!);
return member;
}
private async addToUser(user: SelfUserDocument, guildId: string) {
user.guildIds.push(guildId);
await user.save();
private async addGuildToUser(userId: string, guildId: string) {
await User.updateOne({ _id: userId }, { $push: { guildIds: guildId } });
}
private async getEveryoneRoleId(guildId: string) {

View File

@ -12,7 +12,7 @@ export default class Guilds extends DBWrapper<string, GuildDocument> {
public async get(id: string | undefined) {
const guild = await Guild.findById(id);
if (!guild)
throw new APIError(404, 'Guild Not Found');
throw new APIError(404, 'Guild not found');
return guild;
}
@ -20,8 +20,8 @@ export default class Guilds extends DBWrapper<string, GuildDocument> {
return await Guild.findOne({ channels: { $in: id } as any });
}
public async create(name: string, owner: SelfUserDocument): Promise<GuildDocument> {
const guildId = generateSnowflake();
public async create(options: Partial<Entity.Guild>): Promise<GuildDocument> {
const guildId = options.id ?? generateSnowflake();
const [_, systemChannel, __] = await Promise.all([
deps.roles.create(guildId, { name: '@everyone' }),
@ -31,11 +31,12 @@ export default class Guilds extends DBWrapper<string, GuildDocument> {
const [guild, ___] = await Promise.all([
Guild.create({
_id: guildId,
name,
ownerId: owner.id,
name: 'Unnamed Guild',
ownerId: options.ownerId,
systemChannelId: systemChannel.id,
...options,
}),
deps.guildMembers.create(guildId, owner),
deps.guildMembers.create({ guildId, userId: options.ownerId }),
]);
return guild;

View File

@ -1,7 +1,6 @@
import { Socket } from 'socket.io';
import { Channel } from '../../data/models/channel';
import { SelfUserDocument } from '../../data/models/user';
import { WSGuard } from './ws-guard';
export class WSRooms {
@ -26,7 +25,6 @@ export class WSRooms {
for (const channel of channels)
try {
// TODO: TESTME
if (channel.type === 'VOICE') continue;
await deps.wsGuard.validateCanInChannel(client, channel.id, 'READ_MESSAGES');

View File

@ -43,7 +43,11 @@ export class WebSocket {
for (const event of this.events.values())
client.on(event.on, async (data: any) => {
try {
await event.invoke.call(event, this, client, data);
const actions = await event.invoke.call(event, this, client, data);
for (const action of actions)
this.io
.to(action.to)
.emit(action.emit, action.send);
} catch (error) {
client.emit('error', { message: (error as Error).message });
} finally {

View File

@ -10,7 +10,7 @@ export default class implements WSEvent<'GUILD_CREATE'> {
const userId = ws.sessions.userId(client);
const user = await deps.users.getSelf(userId);
const guild = await deps.guilds.create(name, user);
const guild = await deps.guilds.create({ name, ownerId: user.id });
const entities = await deps.guilds.getEntities(guild.id);
await deps.wsRooms.joinGuildRooms(user, client);

View File

@ -26,7 +26,7 @@ export default class implements WSEvent<'GUILD_MEMBER_ADD'> {
const [_, __, member] = await Promise.all([
this.handleInvite(invite),
deps.wsRooms.joinGuildRooms(selfUser, client),
deps.guildMembers.create(guild.id, selfUser),,
deps.guildMembers.create({ guildId: guild.id, userId: selfUser.id }),
]);
const entities = await deps.guilds.getEntities(guild.id);
client.emit('GUILD_CREATE', { guild, ...entities } as WS.Args.GuildCreate);

View File

@ -1,62 +1,38 @@
import { Socket } from 'socket.io';
import Channels from '../../data/channels';
import { SelfUserDocument } from '../../data/models/user';
import Users from '../../data/users';
import { WSGuard } from '../modules/ws-guard';
import { WSRooms } from '../modules/ws-rooms';
import { WebSocket } from '../websocket';
import ChannelJoin from './channel-join';
import { WSEvent, } from './ws-event';
export default class implements WSEvent<'READY'> {
public on = 'READY' as const;
public cooldown = 5;
constructor(
private channelJoinEvent = deps.channelJoin,
private guard = deps.wsGuard,
private rooms = deps.wsRooms,
private users = deps.users,
) {}
public async invoke(ws: WebSocket, client: Socket, { token }: WS.Params.Ready) {
const { id: userId } = await this.guard.decodeKey(token);
const { id: userId } = await deps.wsGuard.decodeKey(token);
if (!userId)
throw new TypeError('Invalid User ID');
ws.sessions.set(client.id, userId);
const user = await this.users.getSelf(userId);
const user = await deps.users.getSelf(userId);
try {
if (user.voice.channelId)
await this.channelJoinEvent.invoke(ws, client, {
channelId: user.voice.channelId,
});
await deps.channelJoin.invoke(ws, client, { channelId: user.voice.channelId });
} catch {}
await this.handleUser(ws, user);
await this.rooms.join(client, user);
ws.io
.to(client.id)
.emit('READY', { user } as WS.Args.Ready);
}
private async handleUser(ws: WebSocket, user: SelfUserDocument) {
// if (user.status === 'ONLINE') return;
user.status = 'ONLINE';
await user.save();
ws.io
.to(user.guildIds)
.emit('PRESENCE_UPDATE', {
userId: user.id,
status: user.status
} as WS.Args.PresenceUpdate);
await deps.wsRooms.join(client, user);
return [{
emit: 'PRESENCE_UPDATE',
to: user.guildIds,
send: { userId: user.id, status: user.status },
}, {
emit: 'READY',
to: [client.id],
send: { user },
}];
}
}

View File

@ -7,7 +7,6 @@ type OnWS = WS.To & WS.On;
export interface WSEvent<K extends keyof OnWS> {
on: K;
cooldown?: number;
invoke: (ws: WebSocket, client: Socket, params: OnWS[K]) => Promise<WSAction<keyof WS.From>[]>;
}

View File

@ -0,0 +1,80 @@
import '@accord/types';
import { given, test } from '@accord/ion';
import { Channel } from '@accord/backend/data/models/channel';
import { Guild } from '@accord/backend/data/models/guild';
import { SelfUserDocument, User } from '@accord/backend/data/models/user';
import { generateSnowflake } from '@accord/backend/data/snowflake-entity';
import io from 'socket.io-client';
const socket = (io as any).connect(process.env.ROOT_ENDPOINT, {
secure: true,
path: `/ws`,
transports: ['websocket', 'polling', 'flashsocket'],
});
socket.io.on('open', () => console.log('Connected to WS Server'));
test(channelDelete, () => {
const channelId = generateSnowflake();
const guildId = generateSnowflake();
let channel: Entity.Channel;
let guild: Entity.Guild;
let ownerUser: SelfUserDocument;
beforeEach(async function () {
await Promise.all([
Channel.deleteMany(),
Guild.deleteMany(),
User.deleteMany(),
]);
ownerUser = await deps.users.create({
email: 'user1@example.com',
username: 'Test User',
password: 'doesnotmatter',
});
guild = await deps.guilds.create({
id: guildId,
name: 'Test Guild',
ownerId: ownerUser.id,
});
channel = await deps.channels.create({ id: channelId, guildId });
const token = await deps.users.createToken(ownerUser);
socket.emit('READY', { token });
});
// @accord/ion: before tests must go above
given({ channelId })
.message('Channel exists, user is not in guild')
.before(setRandomUser)
.rejectWith('Guild member not found');
given({})
.message('No args, rejected')
.rejectWith('Channel not found');
given({ channelId: generateSnowflake() })
.message('Non existing channel, rejected')
.rejectWith('Channel not found');
given({ channelId })
.message('Channel exists, user is guild owner')
.resolveWith({ channelId, guildId });
async function setRandomUser() {
const randomUser = await deps.users.create({
email: 'user2@example.com',
username: 'Test User 2',
password: 'doesnotmatter',
});
const token = await deps.users.createToken(randomUser);
socket.emit('READY', { token });
}
});
function channelDelete(args: WS.To['CHANNEL_DELETE']) {
return new Promise((resolve, reject) => {
socket.on('CHANNEL_DELETE', (res) => resolve(res));
socket.on('error', (error) => reject(error));
socket.emit('CHANNEL_DELETE', args);
});
}

View File

@ -1,53 +0,0 @@
import '@accord/types';
import { given, test } from '@accord/ion';
import { Channel } from '@accord/backend/data/models/channel';
import { Guild } from '@accord/backend/data/models/guild';
import ChannelDelete from '@accord/backend/ws/ws-events/channel-delete';
import { WebSocket } from '@accord/backend/ws/websocket';
import { SelfUserDocument, User } from '@accord/backend/data/models/user';
import { generateSnowflake } from '@accord/backend/data/snowflake-entity';
test(channelDelete, () => {
let channel: Entity.Channel;
let guild: Entity.Guild;
let ownerUser: SelfUserDocument;
beforeEach(async () => {
await Promise.all([
Channel.deleteMany(),
Guild.deleteMany(),
User.deleteMany(),
]);
ownerUser = await deps.users.create({
email: 'user1@example.com',
username: 'Test User',
password: 'doesnotmatter',
});
guild = await deps.guilds.create('Test Guild', ownerUser);
channel = await deps.channels.create({ guildId: guild.id });
})
// given({ channelId: channel.id })
// .before(async () => ownerUser = await deps.users.create({
// email: 'user2@example.com',
// username: 'Test User 2',
// password: 'doesnotmatter',
// }))
// .rejectWith('Missing Permissions');
// given({ channelId: channel.id }).resolveWith([{
// emit: 'CHANNEL_DELETE',
// to: channel.id,
// send: { channelId: channel.id },
// }]);
given({}).rejectWith('Channel not found');
given({ channelId: generateSnowflake() }).rejectWith('Channel not found');
});
async function channelDelete(args: WS.To['CHANNEL_DELETE']) {
const event = new ChannelDelete();
return event.invoke(new WebSocket(), {} as any, args);
}

View File

@ -8,7 +8,7 @@
"name": "accord-frontend",
"version": "0.0.0",
"dependencies": {
"@accord/types": "file:./types",
"@accord/types": "file:../types",
"@craco/craco": "^5.9.0",
"@dvhb/craco-extend-scope": "^1.0.1",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
@ -77,8 +77,15 @@
"fsevents": "^2.3.2"
}
},
"../types": {
"name": "@accord/types",
"dependencies": {
"@accord/backend": "file:../backend/src",
"@types/winston": "^2.4.4"
}
},
"node_modules/@accord/types": {
"resolved": "types",
"resolved": "../types",
"link": true
},
"node_modules/@babel/code-frame": {
@ -14231,9 +14238,9 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"node_modules/json-schema-traverse": {
@ -14283,18 +14290,18 @@
}
},
"node_modules/jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true,
"engines": [
"node >=0.6.0"
],
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"json-schema": "0.4.0",
"verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/jss": {
@ -26547,11 +26554,17 @@
"resolved": "https://registry.npmjs.org/zalgo-promise/-/zalgo-promise-1.0.48.tgz",
"integrity": "sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ=="
},
"types": {}
"types": {
"extraneous": true
}
},
"dependencies": {
"@accord/types": {
"version": "file:types"
"version": "file:../types",
"requires": {
"@accord/backend": "file:../backend/src",
"@types/winston": "^2.4.4"
}
},
"@babel/code-frame": {
"version": "7.16.0",
@ -37428,9 +37441,9 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"json-schema-traverse": {
@ -37472,14 +37485,14 @@
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"json-schema": "0.4.0",
"verror": "1.10.0"
}
},

View File

@ -4,7 +4,7 @@
"private": true,
"homepage": "https://accord.app",
"dependencies": {
"@accord/types": "file:./types",
"@accord/types": "file:../types",
"@craco/craco": "^5.9.0",
"@dvhb/craco-extend-scope": "^1.0.1",
"@fortawesome/fontawesome-svg-core": "^1.2.35",

View File

@ -9,7 +9,6 @@ import PrivateRoute from './routing/private-route';
import NotFoundPage from './pages/not-found-page';
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import fetchEntities from '../store/actions/fetch-entities';
import { ready } from '../store/auth';
import { initPings } from '../store/pings';
import VerifyPage from './pages/auth/verify-page';

View File

@ -1,3 +1,4 @@
import '@accord/types';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';

View File

@ -1,3 +1,4 @@
import '@accord/types';
import io from 'socket.io-client';
const ws = (io as any).connect(process.env.REACT_APP_ROOT_API_URL, {

View File

@ -6,6 +6,7 @@ declare global {
MONGO_URI: string;
NODE_ENV: 'dev' | 'prod';
PORT: string;
ROOT_ENDPOINT: string;
WEBSITE_URL: string;
}
}

View File

@ -129,6 +129,8 @@ declare namespace WS {
export interface ChannelDelete {
/** ID of the channel to delete. */
channelId: string;
/** ID of the guild that the channel was in. */
guildId: string;
}
export interface ChannelUpdate {
/** ID of the channel to update. */