Skip to content

Commit

Permalink
implement tests and start of user notes
Browse files Browse the repository at this point in the history
  • Loading branch information
mspalmer91 committed May 28, 2023
1 parent 8e6cc90 commit 1e87cd6
Show file tree
Hide file tree
Showing 33 changed files with 653 additions and 67 deletions.
19 changes: 19 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
HTTP_PORT=3010

LOG_PATH=./logs
SESSION_SECRET=changeme

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=redose_api
POSTGRES_PASSWORD=P@ssw0rd
POSTGRES_DATABASE=redose_test

SMTP_HOST=localhost
SMTP_PORT=465
SMTP_USER=redose_api
SMTP_PASSWORD=P@ssw0rd

DISCORD_CLIENT_ID=mockDiscordClientId
DISCORD_CLIENT_SECRET=mockDiscordClientSecret
DISCORD_HOME_GUILD_ID=mockDiscordGuildId
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ module.exports = {
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/lines-between-class-members': 0,
},
},
{
files: [
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ typings/

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
6 changes: 2 additions & 4 deletions db-scripts/initialize-database.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#!/bin/bash
set -e

TEST_USER="${POSTGRES_USER}_test"
TEST_DATABASE="${POSTGRES_DATABASE}_test"

psql -v ON_ERROR_STOP=1 -U "${POSTGRES_USER}" <<-EOSQL
CREATE USER ${TEST_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';
CREATE DATABASE ${POSTGRES_DATABASE};
CREATE DATABASE ${TEST_DATABASE};
GRANT ALL PRIVILEGES ON DATABASE ${POSTGERS_DATABASE} TO ${POSTGRES_USER};
GRANT ALL PRIVILEGES ON DATABASE ${TEST_DATABASE} TO ${TEST_USER};
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DATABASE} TO ${POSTGRES_USER};
GRANT ALL PRIVILEGES ON DATABASE ${TEST_DATABASE} TO ${POSTGRES_USER};
EOSQL
8 changes: 7 additions & 1 deletion jest.integration.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
clearMocks: true,
globalSetup: '<rootDir>/tests/setup.ts',
globalTeardown: '<rootDir>/tests/teardown.ts',
setupFilesAfterEnv: [
'<rootDir>/tests/setup-env.ts',
],
testMatch: [
'./tests/**/*.spec.ts',
'<rootDir>/tests/spec/**/*.spec.ts',
],
};
41 changes: 41 additions & 0 deletions migrations/20230424094928_sessions-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,46 @@ export async function up(knex: Knex): Promise<void> {

table.string('email', 320);

table
.timestamp('createdAt')
.notNullable()
.defaultTo(knex.fn.now());
})
.createTable('userNotes', (table) => {
table
.uuid('id')
.notNullable()
.defaultTo(knex.raw('uuid_generate_v4()'))
.primary();

table
.text('userId')
.notNullable()
.references('id')
.inTable('users');

table.text('channelId');

table
.text('content')
.notNullable();

table
.boolean('deleted')
.notNullable()
.defaultTo(false);

table
.timestamp('updatedAt')
.notNullable()
.defaultTo(knex.fn.now());

table
.text('createdBy')
.notNullable()
.references('id')
.inTable('users');

table
.timestamp('createdAt')
.notNullable()
Expand All @@ -85,6 +125,7 @@ export async function up(knex: Knex): Promise<void> {

export async function down(knex: Knex): Promise<void> {
await knex.schema
.dropTableIfExists('userNotes')
.dropTableIfExists('emergencyContacts')
.dropTableIfExists('webSessions')
.dropTableIfExists('users');
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev": "NODE_ENV=development nodemon --exec \"node -r ts-node/register --inspect\" ./src/index.ts",
"test": "yarn lint && yarn test:jest",
"test:unit": "NODE_ENV=test jest",
"test:integration": "NODE_ENV=test jest -c jest.integration.config.js",
"test:integration": "NODE_ENV=test jest -ic jest.integration.config.js",
"lint": "eslint --ext .ts,.js .",
"register-commands": "NODE_ENV=production ts-node ./register-commands.ts --noEmit",
"db:migrate": "knex migrate:latest",
Expand Down Expand Up @@ -46,6 +46,7 @@
"@types/mjml": "^4.7.1",
"@types/node": "^18.16.0",
"@types/nodemailer": "^6.4.7",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.2.0",
Expand All @@ -60,6 +61,7 @@
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"supertest-session": "^4.1.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
Expand Down
8 changes: 8 additions & 0 deletions src/__mocks__/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function createMockLogger() {
return {
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
}
57 changes: 57 additions & 0 deletions src/discord-client/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Client,
ClientOptions,
CommandInteraction,
User,
} from 'discord.js';

export default async function createMockDiscordClient() {
return class MockDiscordClient {
client!: Client;
user!: User;
interaction!: CommandInteraction;
#options?: ClientOptions;

constructor(options?: ClientOptions) {
this.#options = options;
this.#mockClient();
this.#mockUser();
this.#mockInteractions();
}

getInteractions(): CommandInteraction {
return this.interaction;
}

#mockClient() {
this.client = new Client({ intents: [] });
this.client.login = jest.fn(() => Promise.resolve('MOCK_LOGIN_TOKEN'));
}

#mockUser() {
this.user = Reflect.construct(User, [
this.client,
{
id: 'mockUserId',
username: 'mockUsername',
discriminator: 'mockDiscriminator#0001',
avatar: 'mockUserAvatar.png',
bot: false,
},
]);
}

#mockInteractions() {
this.interaction = Reflect.construct(CommandInteraction, [
this.client,
{
data: this.#options,
id: BigInt(1),
user: this.user,
},
]);
this.interaction.reply = jest.fn();
this.interaction.isCommand = jest.fn(() => true);
}
};
}
53 changes: 53 additions & 0 deletions src/discord-client/commands/note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { UserNote } from '@redose/types';
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import type { Command } from '.';

const noteCommand: Command = {
meta: new SlashCommandBuilder()
.setName('note')
.setDescription('Manage and create notes associated with a user')
.addSubcommand((subcommand) => subcommand
.setName('add')
.setDescription('Create a new note for a user')
.addUserOption((option) => option
.setName('user')
.setDescription('User to associate note to')
.setRequired(true))
.addStringOption((option) => option
.setName('contents')
.setDescription('Contents of the note')
.setRequired(true))),

async execute(interaction, { knex }) {
const userId = interaction.options.getUser('user')!.id;
if (userId === interaction.user.id) {
return interaction.reply({
ephemeral: true,
embeds: [
new EmbedBuilder()
.setTitle('note add - Error')
.setDescription('You cannot repeat yourself.')
.setColor('#fe3333'),
],
});
}

await knex<UserNote>('userNotes').insert({
userId,
content: interaction.options.getString('content')!,
createdBy: interaction.user.id,
});

return interaction.reply({
ephemeral: true,
embeds: [
new EmbedBuilder()
.setTitle('note add - Success')
.setDescription('Note successfully created.')
.setColor('#33fe33'),
],
});
},
};

export default noteCommand;
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function createRedoseApi() {
knex: createKnex(knexConfig),
suuid: shortUUID(),
mail: await createMailService(logger)
.catch(createErrorHandler('Error creating mail service'))
.catch(createErrorHandler('Error creating mail service')),
};

const serverDeps = {
Expand Down
22 changes: 14 additions & 8 deletions src/mail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const TEMPLATE_NAMES: Array<keyof Emails> = [
'verify',
];

export default async function createMailService(logger: Logger): Promise<Emails> {
export default async function createMailService(logger: Logger) {
const transport = createTransport({
secure: SMTP_SECURE,
host: SMTP_HOST,
Expand All @@ -47,11 +47,17 @@ export default async function createMailService(logger: Logger): Promise<Emails>
});
}

return Object.fromEntries(await Promise.all(TEMPLATE_NAMES
.map((name) => compileTemplate(name)
.then((send) => [name, send])
.catch((ex) => {
logger.error('Failed to compile template:', ex);
return Promise.reject(ex);
}))));
return {
send: Object.fromEntries(await Promise.all(TEMPLATE_NAMES
.map((name) => compileTemplate(name)
.then((send) => [name, send])
.catch((ex) => {
logger.error('Failed to compile template:', ex);
return Promise.reject(ex);
})))),

async close() {
return transport.close();
},
};
}
10 changes: 10 additions & 0 deletions src/middleware/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { RequestHandler } from 'express';
import type { Deps } from '..';

type PermissionRoles = 'Admin' | 'Moderator' | 'Responder';

export default function authorize({ discordClient }: Deps, roleIds: string[]): RequestHandler {
return async (req, res, next) => {
next();
};
}
2 changes: 2 additions & 0 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as authorize } from './authorize';
export { default as isAuthenticated } from './is-authenticated';
export { default as meUrlParam } from './me-url-param';
export { default as defaultErrorHandler } from './default-error-handler';
export { default as joiErrorHandler } from './joi-error-handler';
export { default as withCurrentUser } from './with-current-user';
2 changes: 1 addition & 1 deletion src/middleware/is-authenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import type { RequestHandler } from 'express';
export default function isAuthenticated(inverse: boolean = false): RequestHandler {
return (req, res, next) => {
if ((!inverse && req.session?.userId) || (inverse && !req.session?.userId)) next();
else res.sendStatus(401);
else res.sendStatus(inverse ? 403 : 401);
};
}
16 changes: 16 additions & 0 deletions src/middleware/with-current-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { RequestHandler } from 'express';
import type { Deps } from '..';

export default function authorize(deps: Deps): RequestHandler {
const { discordClient } = deps;

return async (req, res, next) => {
if (!req.session.userId) {
throw new Error('authorize middleware should be used after isAuthenticated');
}
Object.assign(res.locals, {
user: await discordClient.users.fetch(req.session.userId),
});
next();
};
}
2 changes: 2 additions & 0 deletions src/router/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { applyRoutes } from '../../utils';
import userRoutes from './user';
import sessionRoutes from './session';
import noteRoutes from './note';
import applyEmergencyInfoRoutes from './emergency-info';

const applyUserRoutes = applyRoutes(
userRoutes,
noteRoutes,
sessionRoutes,
applyEmergencyInfoRoutes,
);
Expand Down
Loading

0 comments on commit 1e87cd6

Please sign in to comment.