Backend: Fix Test Types

This commit is contained in:
ADAMJR 2021-11-11 16:07:41 +00:00
parent 1e6af8267a
commit 080b11fb31
21 changed files with 78 additions and 69 deletions

View File

@ -8,6 +8,8 @@ Custom Frontend and Backend that is similar to Discord.
<a href="https://ibb.co/st2q2B0"><img src="https://i.ibb.co/fQ2H2ch/Screenshot-from-2021-08-30-11-55-01.png" alt="Screenshot-from-2021-08-30-11-55-01" border="0" /></a>
<a href="https://ibb.co/SydPgTY"><img src="https://i.ibb.co/qjWd8Gq/Screenshot-from-2021-08-30-13-30-43.png" alt="Screenshot-from-2021-08-30-13-30-43" border="0" /></a>
> Looking for a full Discord API Clone? Then check out [fosscord](https://github.com/fosscord/fosscord).
---
## Setup

View File

@ -25,6 +25,16 @@ export default class Messages extends DBWrapper<string, MessageDocument> {
});
}
public async createSystem(guildId: string, content: string) {
const { systemChannelId: channelId } = await deps.guilds.get(guildId);
return await Message.create({
_id: generateSnowflake(),
channelId,
content,
});
}
public async getChannelMessages(channelId: string) {
return await Message.find({ channelId });
}

View File

@ -1,6 +1,7 @@
import { Document, model, Schema } from 'mongoose';
import patterns from '../../types/patterns';
import { createdAtToDate, useId } from '../../utils/utils';
import validators from '../../utils/validators';
import { generateSnowflake } from '../snowflake-entity';
// properties we don't need to define when creating a guild
@ -29,6 +30,10 @@ export const Guild = model<GuildDocument>('guild', new Schema({
required: true,
validate: [patterns.snowflake, 'Invalid Snowflake ID'],
},
systemChannelId: {
type: String,
validate: [validators.optionalSnowflake, 'Invalid Snowflake ID'],
}
},
{ toJSON: { getters: true } })
.method('toClient', useId));

View File

@ -39,6 +39,7 @@ export const Message = model<MessageDocument>('message', new Schema({
title: String,
url: String,
}),
system: Boolean,
updatedAt: Date,
}, { toJSON: { getters: true } })
.method('toClient', useId));

View File

@ -1,14 +1,8 @@
import Channels from '../../data/channels';
import { WS } from '../../types/ws';
import { WSEvent } from './ws-event';
import { WebSocket } from '../websocket';
import { Socket } from 'socket.io';
import { VoiceService } from '../../voice/voice-service';
import Users from '../../data/users';
import { SelfUserDocument } from '../../data/models/user';
import ChannelLeave from './channel-leave';
import VoiceData from './voice-data';
export default class implements WSEvent<'CHANNEL_JOIN'> {
on = 'CHANNEL_JOIN' as const;

View File

@ -5,7 +5,6 @@ import Invites from '../../data/invites';
import { InviteDocument } from '../../data/models/invite';
import Users from '../../data/users';
import { WS } from '../../types/ws';
import { WSRooms } from '../modules/ws-rooms';
import { WebSocket } from '../websocket';
import { WSEvent, } from './ws-event';
@ -13,36 +12,28 @@ import { WSEvent, } from './ws-event';
export default class implements WSEvent<'GUILD_MEMBER_ADD'> {
on = 'GUILD_MEMBER_ADD' as const;
constructor(
private guilds = deps.guilds,
private members = deps.guildMembers,
private invites = deps.invites,
private rooms = deps.wsRooms,
private users = deps.users,
) {}
public async invoke(ws: WebSocket, client: Socket, { inviteCode }: WS.Params.GuildMemberAdd) {
const invite = await this.invites.get(inviteCode);
const guild = await this.guilds.get(invite.guildId);
const invite = await deps.invites.get(inviteCode);
const guild = await deps.guilds.get(invite.guildId);
const userId = ws.sessions.userId(client);
const members = await this.guilds.getMembers(guild.id);
const members = await deps.guilds.getMembers(guild.id);
const inGuild = members.some(m => m.userId === userId);
if (inGuild)
throw new TypeError('User already in guild');
const selfUser = await this.users.getSelf(userId);
const selfUser = await deps.users.getSelf(userId);
if (inviteCode && selfUser.bot)
throw new TypeError('Bot users cannot accept invites');
const [_, __, member] = await Promise.all([
this.handleInvite(invite),
this.rooms.joinGuildRooms(selfUser, client),
this.members.create(guild.id, selfUser),,
deps.wsRooms.joinGuildRooms(selfUser, client),
deps.guildMembers.create(guild.id, selfUser),,
]);
const entities = await this.guilds.getEntities(guild.id);
const entities = await deps.guilds.getEntities(guild.id);
client.emit('GUILD_CREATE', { guild, ...entities } as WS.Args.GuildCreate);
ws.io
.to(guild.id)
.emit('GUILD_MEMBER_ADD', {
@ -52,6 +43,8 @@ export default class implements WSEvent<'GUILD_MEMBER_ADD'> {
} as WS.Args.GuildMemberAdd);
await client.join(guild.id);
await deps.messages.createSystem(guild.id, `<@${selfUser.id}> joined the guild.`);
}
private async handleInvite(invite: InviteDocument) {

View File

@ -8,7 +8,7 @@ import { WS } from '../../types/ws';
export default class implements WSEvent<'GUILD_UPDATE'> {
on = 'GUILD_UPDATE' as const;
public async invoke(ws: WebSocket, client: Socket, { guildId, name, iconURL }: WS.Params.GuildUpdate) {
public async invoke(ws: WebSocket, client: Socket, { guildId, name, iconURL, systemChannelId }: WS.Params.GuildUpdate) {
await deps.wsGuard.validateCan(client, guildId, 'MANAGE_GUILD');
const guild = await deps.guilds.get(guildId);
@ -17,6 +17,7 @@ export default class implements WSEvent<'GUILD_UPDATE'> {
if (hasChanged('iconURL', iconURL)) partial.iconURL = iconURL;
if (hasChanged('name', name)) partial.name = name!;
if (hasChanged('systemChannelId', systemChannelId)) partial.systemChannelId = systemChannelId!;
Object.assign(guild, partial);
await guild.save();

View File

@ -22,6 +22,8 @@ export default class implements WSEvent<'MESSAGE_CREATE'> {
author.lastReadMessageIds[channelId] = message.id;
await author.save();
// await deps.messages.createSystem(.id, `<@${selfUser.id}> sent a message.`);
ws.io
.to(channelId)
.emit('MESSAGE_CREATE', { message } as WS.Args.MessageCreate);

View File

@ -33,30 +33,6 @@ use(should);
execSync(`kill -9 $(lsof -i :${process.env.PORT} | tail -n 1 | cut -d ' ' -f5) 2>> /dev/null`);
} catch {}
import('./integration/routes/auth-routes.tests');
import('./integration/routes/invites-routes.tests');
import('./integration/routes/guilds-routes.tests');
import('./integration/routes/channel-routes.tests');
import('./integration/ws/channel-create.tests');
// TODO: import('./integration/ws/channel-delete.tests');
// TODO: import('./integration/ws/channel-update.tests');
import('./integration/ws/guild-member-add.tests'); //fail
import('./integration/ws/guild-member-remove.tests'); //fail
import('./integration/ws/guild-member-update.tests'); // fail
import('./integration/ws/guild-create.tests'); // fail
import('./integration/ws/guild-delete.tests'); // fail
import('./integration/ws/guild-update.tests'); // fail
import('./integration/ws/invite-create.tests'); //fail
import('./integration/ws/invite-delete.tests'); //fail
import('./integration/ws/message-create.tests'); //fail
import('./integration/ws/message-update.tests'); // fail
import('./integration/ws/message-delete.tests'); // fail
import('./integration/ws/ready.tests');
import('./integration/ws/user-update.tests');
import('./integration/ws/ws-guard.tests');
import('./integration/data/roles.tests');
import('./unit/models/app.tests');
import('./unit/models/channel.tests');
import('./unit/models/guild.tests');
@ -70,5 +46,4 @@ use(should);
})();
// needs to be here, or tests won't run
describe('oh', () => it('frick', () => expect(true).to.be.true));
// after(() => process.exit(0));
describe('oh', () => it('frick', () => expect(true).to.be.true));

View File

@ -1,7 +1,7 @@
import { Channel } from '../../../src/data/models/channel';
import { generateSnowflake } from '../../../src/data/snowflake-entity';
import { test, given } from 'sazerac';
import { longArray, longString, mongooseError } from '../../test-utils';
import { longString, mongooseError } from '../../test-utils';
test(createChannel, () => {
given().expect(true);
@ -10,6 +10,9 @@ test(createChannel, () => {
given({ guildId: null }).expect(true);
given({ guildId: '123' }).expect('Invalid Snowflake ID');
given({ guildId: generateSnowflake() }).expect(true);
given({ lastMessageId: generateSnowflake() }).expect(true);
given({ lastMessageId: '' }).expect(true);
given({ lastMessageId: '123' }).expect('Invalid Snowflake ID');
given({ name: '' }).expect('Name is required');
given({ name: longString(33) }).expect('Name too long');
given({ name: 'channel-name' }).expect(true);
@ -18,16 +21,13 @@ test(createChannel, () => {
given({ name: 'channel name', type: 'DM' }).expect(true);
given({ summary: longString(129) }).expect('Summary too long');
given({ summary: 'cool channel' }).expect(true);
given({ systemChannelId: generateSnowflake() }).expect(true);
given({ systemChannelId: '' }).expect(true);
given({ systemChannelId: '123' }).expect('Invalid Snowflake ID');
given({ type: 'A' }).expect('Invalid type');
given({ type: 'TEXT' }).expect(true);
given({ type: 'VOICE' }).expect(true);
given({ type: 'DM' }).expect(true);
given({ firstMessageId: generateSnowflake() }).expect(true);
given({ firstMessageId: '' }).expect(true);
given({ firstMessageId: '123' }).expect('Invalid Snowflake ID');
given({ lastMessageId: generateSnowflake() }).expect(true);
given({ lastMessageId: '' }).expect(true);
given({ lastMessageId: '123' }).expect('Invalid Snowflake ID');
});
function createChannel(channel: Partial<Entity.Channel>) {

View File

@ -18,5 +18,9 @@
"skipLibCheck": true,
"watch": true,
},
"include": ["src", "env"]
"include": [
"src",
"env",
"**/*.ts"
]
}

View File

@ -1,5 +1,7 @@
{
"baseUrl": "http://localhost:4200",
"env": {
"baseUrl": "http://localhost:4200"
},
"fixturesFolder": "e2e/fixtures",
"integrationFolder": "e2e/integration",
"pluginsFile": "e2e/plugins/index.ts",

View File

@ -1,6 +1,5 @@
import './message.scoped.css';
import './message.global.css';
import moment from 'moment';
import { useDispatch, useSelector } from 'react-redux';
import { getChannelMessages } from '../../../store/messages';
@ -60,11 +59,17 @@ const Message: React.FunctionComponent<MessageProps> = ({ message }: MessageProp
<div className="absolute toolbar right-0 -mt-3 z-10">
<MessageToolbar message={message} />
</div>
<MessageHeader
author={author}
message={message}
isExtra={isActuallyExtra} />
<MessageContent message={message} />
{message.system
? (<div>{'->'} {message.content}</div>)
: (
<>
<MessageHeader
author={author}
message={message}
isExtra={isActuallyExtra} />
<MessageContent message={message} />
</>
)}
{/* <MessageEmbed embed={{
title: 'Never Gonna Give You Up',
description: 'Never going to let you down',

View File

@ -18,7 +18,7 @@ const GuildSettingsInvites: React.FunctionComponent = () => {
<span className="float-right">
<button
type="button"
className="danger rounded-full ring ring-red-500 px-2"
className="danger rounded-full border-2 border-red-500 px-2"
onClick={() => dispatch(deleteInvite(i.id))}>x</button>
</span>
</div>

View File

@ -1,16 +1,18 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { deleteGuild, updateGuild, uploadGuildIcon } from '../../../store/guilds';
import { deleteGuild, getGuildChannels, updateGuild, uploadGuildIcon } from '../../../store/guilds';
import { openSaveChanges } from '../../../store/ui';
import NormalButton from '../../utils/buttons/normal-button';
import Category from '../../utils/category';
import Input from '../../utils/input/input';
import SaveChanges from '../../utils/save-changes';
import Select from 'react-select';
const GuildSettingsOverview: React.FunctionComponent = () => {
const dispatch = useDispatch();
const guild = useSelector((s: Store.AppState) => s.ui.activeGuild)!;
const channels = useSelector(getGuildChannels(guild.id));
const { register, handleSubmit, setValue } = useForm();
const onSave = (e) => {
@ -49,6 +51,11 @@ const GuildSettingsOverview: React.FunctionComponent = () => {
const file = e.currentTarget?.files?.[0];
if (file) dispatch(uploadGuildIcon(guild.id, file));
}} />
{/* TODO: move to channel-select */}
<Select
options={channels
.filter(c => c.type === 'TEXT')
.map(c => ({ label: `#${c.name}`, value: c.id }))} />
</section>
<Category

View File

@ -5,7 +5,7 @@ const CircleButton: React.FunctionComponent<any> = (props) => {
<button
{...props}
className={classNames(
`rounded-full ring ring-gray-400 secondary px-4 py-1`,
`rounded-full border-2 border-gray-400 secondary px-4 py-1`,
props.className)}>{props.children}</button>
);
}

View File

@ -17,7 +17,7 @@ const EscButton: React.FunctionComponent = () => {
return (
<div
id="escButton"
className="rounded-full ring ring-gray-500 cursor-pointer border-white rounded-full px-2 w-16 mt-14"
className="rounded-full border-2 border-gray-500 cursor-pointer border-white rounded-full px-2 w-16 mt-14"
onClick={onClick}>
<FontAwesomeIcon icon={faTimes} color="var(--muted)" />
<span className="pl-1.5 muted">ESC</span>

View File

@ -110,6 +110,7 @@ const WSListener: React.FunctionComponent = () => {
dispatch(invites.created(args));
dispatch(uiActions.focusedInvite(args.invite));
});
ws.on('INVITE_DELETE', (args) => dispatch(invites.deleted(args)));
ws.on('MESSAGE_CREATE', (args) => {
const selfUser = state().auth.user!;
const isBlocked = selfUser.ignored?.userIds.includes(args.message.authorId);

View File

@ -18,6 +18,10 @@ const slice = createSlice({
created: ({ list }, { payload }: Store.Action<WS.Args.InviteCreate>) => {
list.push(payload.invite);
},
deleted: ({ list }, { payload }: Store.Action<WS.Args.InviteDelete>) => {
const index = list.findIndex(i => i.id === payload.inviteCode);
list.splice(index, 1);
},
},
});

2
types/entity.d.ts vendored
View File

@ -25,6 +25,7 @@ declare namespace Entity {
createdAt: Date;
iconURL?: string;
ownerId: string;
systemChannelId?: string;
}
export interface GuildMember {
/** @deprecated Not the same as user ID. */
@ -51,6 +52,7 @@ declare namespace Entity {
createdAt: Date;
embed?: MessageTypes.Embed;
updatedAt?: Date;
system?: boolean;
}
export interface Role {
id: string;

1
types/ws.d.ts vendored
View File

@ -184,6 +184,7 @@ declare namespace WS {
guildId: string;
name?: string;
iconURL?: string;
systemChannelId?: string;
}
export interface InviteCreate {
guildId: string;