From d5f5d5ee9c46049bb9c3c41ca5c6ce464ddd9b7c Mon Sep 17 00:00:00 2001 From: Michal Drla Date: Sun, 27 Aug 2023 18:16:04 +0200 Subject: [PATCH] feat: Add mongodb as a default database for application Signed-off-by: Michal Drla --- jest.config.ts | 1 + package-lock.json | 78 ++++++++++++++++ package.json | 5 ++ src/app.ts | 2 + src/commands/verify.ts | 78 +++++++++------- src/model/index.ts | 1 + src/model/prisma.ts | 3 + src/model/schema.prisma | 15 ++++ src/utils/assignRole.ts | 29 +++--- tests/utils/singletonPrisma.ts | 15 ++++ tests/verify.test.ts | 157 +++++++++++++++++---------------- 11 files changed, 258 insertions(+), 126 deletions(-) create mode 100644 src/model/index.ts create mode 100644 src/model/prisma.ts create mode 100644 src/model/schema.prisma create mode 100644 tests/utils/singletonPrisma.ts diff --git a/jest.config.ts b/jest.config.ts index 657386f..63e5f00 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,4 +2,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + setupFilesAfterEnv: ['/tests/utils/singletonPrisma.ts'], }; diff --git a/package-lock.json b/package-lock.json index ac35400..9f0e7b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@discordjs/rest": "2.0.0", + "@prisma/client": "^5.2.0", "discord-api-types": "0.37.52", "discord.js": "14.12.1", "dotenv": "16.3.1", @@ -27,6 +28,7 @@ "eslint": "8.47.0", "jest": "29.6.2", "prettier": "3.0.1", + "prisma": "^5.2.0", "ts-jest": "29.1.1", "ts-node": "10.9.1", "typescript": "5.1.6" @@ -1247,6 +1249,38 @@ "node": ">= 8" } }, + "node_modules/@prisma/client": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.2.0.tgz", + "integrity": "sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" + }, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.2.0.tgz", + "integrity": "sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz", + "integrity": "sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==" + }, "node_modules/@sapphire/async-queue": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", @@ -4547,6 +4581,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.2.0.tgz", + "integrity": "sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.2.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6282,6 +6332,25 @@ "fastq": "^1.6.0" } }, + "@prisma/client": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.2.0.tgz", + "integrity": "sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==", + "requires": { + "@prisma/engines-version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" + } + }, + "@prisma/engines": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.2.0.tgz", + "integrity": "sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz", + "integrity": "sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==" + }, "@sapphire/async-queue": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", @@ -8689,6 +8758,15 @@ } } }, + "prisma": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.2.0.tgz", + "integrity": "sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==", + "devOptional": true, + "requires": { + "@prisma/engines": "5.2.0" + } + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/package.json b/package.json index a5f6532..51ccc72 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "homepage": "https://github.com/OqixDevs/OqixTS#readme", "dependencies": { "@discordjs/rest": "2.0.0", + "@prisma/client": "^5.2.0", "discord-api-types": "0.37.52", "discord.js": "14.12.1", "dotenv": "16.3.1", @@ -47,11 +48,15 @@ "eslint": "8.47.0", "jest": "29.6.2", "prettier": "3.0.1", + "prisma": "^5.2.0", "ts-jest": "29.1.1", "ts-node": "10.9.1", "typescript": "5.1.6" }, "optionalDependencies": { "nodemon": "3.0.1" + }, + "prisma": { + "schema": "src/model/schema.prisma" } } diff --git a/src/app.ts b/src/app.ts index b3b7e0e..986024c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,7 +15,9 @@ import { SubjectChannels } from './utils'; export default () => { dotenv.config(); const token = process.env.DISCORD_TOKEN; // add your token here + console.log('Bot is starting...'); + const client = new Client({ intents: [ GatewayIntentBits.Guilds, diff --git a/src/commands/verify.ts b/src/commands/verify.ts index c6016b9..6392548 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -1,7 +1,7 @@ import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; import { assignRole, scrapeConfirmationStudies, scrapeThesis } from '../utils'; -import * as fs from 'fs'; +import { prisma } from '../model'; /** * Takes URL to confirmation of studies and thesis and if the data scraped from websites are correct verifies user with role. @@ -28,14 +28,6 @@ export const data = new SlashCommandBuilder() ); export async function execute(interaction: ChatInputCommandInteraction) { - let userLog = undefined; - try { - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } catch (e) { - fs.writeFileSync('./userLog.json', '[]'); - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } - const userLogJSON = JSON.parse(userLog); const idConfirmationMuni = interaction.options.getString( 'linktoconfirmationmuni' ); @@ -50,39 +42,61 @@ export async function execute(interaction: ChatInputCommandInteraction) { const idConfirmationMuniParsedUrl = new URL( sanitizeUrl(idConfirmationMuni) ); - for (const key in userLogJSON) { - if (interaction.user.id === userLogJSON[key].id) { - return interaction.reply({ - content: - 'User already verified! Contact admin if you need to verify again.', - ephemeral: true, - }); - } else if ( - userLogJSON[key].idThesis === - bachelorThesisParsedUrl.pathname.split('/')[3] - ) { - return interaction.reply({ - content: 'This thesis is already used! Please contact admin.', - ephemeral: true, - }); - } + + await interaction.reply({ + content: 'Verifying, wait please...', + ephemeral: true, + }); + let user; + try { + user = await prisma.users.findMany({ + where: { + discordId: interaction.user.id, + }, + }); + } catch (err) { + console.log(`Database error: ${err}`); + return interaction.editReply({ + content: 'Verification failed! Contact admin.', + }); + } + if (user.length != 0) { + return interaction.editReply({ + content: + 'User already verified! Contact admin if you need to verify again.', + }); + } + try { + user = await prisma.users.findMany({ + where: { + idThesis: bachelorThesisParsedUrl.pathname.split('/')[3], + }, + }); + } catch (err) { + console.log(`Database error: ${err}`); + return interaction.editReply({ + content: 'Verification failed! Contact admin.', + }); + } + if (user.length != 0) { + return interaction.editReply({ + content: 'This thesis is already used! Please contact admin.', + }); } const authorName = await scrapeThesis(bachelorThesisParsedUrl.pathname); if (!authorName) { - return interaction.reply({ + return interaction.editReply({ content: 'Could not get the author name. Maybe thesis URL is wrong or the website did not respond.', - ephemeral: true, }); } const scrapedConfirmationStudy = await scrapeConfirmationStudies( idConfirmationMuniParsedUrl.pathname ); if (!scrapedConfirmationStudy) { - return interaction.reply({ + return interaction.editReply({ content: 'Could not get infromation from the confirmation of studies. Maybe confirmation of studies URL is wrong or the website did not respond.', - ephemeral: true, }); } @@ -93,17 +107,15 @@ export async function execute(interaction: ChatInputCommandInteraction) { bachelorThesisParsedUrl ); if (roleProgramm) { - return interaction.reply({ + return interaction.editReply({ content: 'You have been successfully verified with role ' + roleProgramm.name + '.', - ephemeral: true, }); } - return interaction.reply({ + return interaction.editReply({ content: 'Verification failed check if you entered the correct information or contact admin.', - ephemeral: true, }); } diff --git a/src/model/index.ts b/src/model/index.ts new file mode 100644 index 0000000..964904b --- /dev/null +++ b/src/model/index.ts @@ -0,0 +1 @@ +export * from './prisma'; diff --git a/src/model/prisma.ts b/src/model/prisma.ts new file mode 100644 index 0000000..9b6c4ce --- /dev/null +++ b/src/model/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); diff --git a/src/model/schema.prisma b/src/model/schema.prisma new file mode 100644 index 0000000..f49534a --- /dev/null +++ b/src/model/schema.prisma @@ -0,0 +1,15 @@ +generator client { + provider = "prisma-client-js" +} + +datasource database { + provider = "mongodb" + url = env("DATABASE_CONNECTION_STRING") +} + +model Users { + id String @id @default(auto()) @map("_id") @database.ObjectId + discordId String @map("id") + idThesis String? + status String +} diff --git a/src/utils/assignRole.ts b/src/utils/assignRole.ts index 2c7a202..6988c10 100644 --- a/src/utils/assignRole.ts +++ b/src/utils/assignRole.ts @@ -1,5 +1,5 @@ import { CommandInteraction, GuildMember } from 'discord.js'; -import * as fs from 'fs'; +import { prisma } from '../model'; interface Dictionary { [Key: string]: T; @@ -25,14 +25,6 @@ export async function assignRole( authorName: string, bachelorThesisParsedUrl: URL ) { - let userLog = undefined; - try { - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } catch (e) { - fs.writeFileSync('./userLog.json', '[]'); - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } - const userLogJSON = JSON.parse(userLog); const today = new Date(); if ( scrapedConfirmationStudy[ @@ -59,12 +51,19 @@ export async function assignRole( programme_roles[scrapedConfirmationStudy['Programme']] ); const member = interaction.member as GuildMember; - userLogJSON.push({ - id: interaction.user.id, - idThesis: bachelorThesisParsedUrl.pathname.split('/')[3], - status: 'verified', - }); - fs.writeFileSync('./userLog.json', JSON.stringify(userLogJSON)); + try { + await prisma.users.create({ + data: { + discordId: interaction.user.id, + idThesis: + bachelorThesisParsedUrl.pathname.split('/')[3], + status: 'verified', + }, + }); + } catch (err) { + console.log(`Database error: ${err}`); + return undefined; + } if (roleVerified && roleProgramm) { member.roles.add(roleVerified); member.roles.add(roleProgramm); diff --git a/tests/utils/singletonPrisma.ts b/tests/utils/singletonPrisma.ts new file mode 100644 index 0000000..c93e7fb --- /dev/null +++ b/tests/utils/singletonPrisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from '@prisma/client'; +import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'; + +import { prisma } from '../../src/model'; + +jest.mock('../../src/model', () => ({ + __esModule: true, + prisma: mockDeep(), +})); + +beforeEach(() => { + mockReset(prismaMock); +}); + +export const prismaMock = prisma as unknown as DeepMockProxy; diff --git a/tests/verify.test.ts b/tests/verify.test.ts index cb33958..c62fd04 100644 --- a/tests/verify.test.ts +++ b/tests/verify.test.ts @@ -2,7 +2,7 @@ import { mockDeep } from 'jest-mock-extended'; import { ChatInputCommandInteraction, Role } from 'discord.js'; import { verify } from '../src/commands'; import * as utils from '../src/utils'; -import * as fs from 'fs'; +import { prismaMock } from './utils/singletonPrisma'; const dict = { Student: undefined, @@ -16,23 +16,23 @@ const dict = { }; let nameUser = ' Josef Novak'; +const databaseUser = { + id: '123', + discordId: '123', + idThesis: '2223121', + status: 'verified', +}; + describe('Tests for verify command', () => { const interaction = mockDeep(); beforeEach(() => { jest.useFakeTimers(); jest.setSystemTime(new Date(2022, 6, 13)); - fs.writeFileSync('./userLog.json', JSON.stringify([])); - }); - - afterEach(() => { - try { - fs.unlinkSync('./userLog.json'); - } catch (e) { - console.log('Could not delete userLog.json - ignoring'); - } }); it('Calling execute should call reply', async () => { + prismaMock.users.findMany.mockResolvedValue([]); + prismaMock.users.create.mockResolvedValue(databaseUser); interaction.user.id = '123'; interaction.options.getString .calledWith('linktoconfirmationmuni') @@ -54,13 +54,13 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ + expect(interaction.editReply).toHaveBeenCalledWith({ content: 'You have been successfully verified with role undefined.', - ephemeral: true, }); }); it('Confirmation name is not same as in Dspace', async () => { + prismaMock.users.findMany.mockResolvedValue([]); dict.Name = 'Jan Bazina'; interaction.user.id = '123'; interaction.options.getString @@ -83,15 +83,16 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ - content: 'You have been successfully verified with role undefined.', //This name has to be here because of the mock - ephemeral: true, + expect(interaction.editReply).toHaveBeenCalledWith({ + content: + 'Verification failed check if you entered the correct information or contact admin.', }); }); it('Dspace name is not same as on confirmation', async () => { nameUser = 'Jan Bazina'; interaction.user.id = '123'; + prismaMock.users.findMany.mockResolvedValue([]); interaction.options.getString .calledWith('idconfirmationmuni') .mockReturnValue( @@ -112,13 +113,13 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ + expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Verification failed check if you entered the correct information or contact admin.', - ephemeral: true, }); }); it('Date in system is not correct', async () => { + prismaMock.users.findMany.mockResolvedValue([]); jest.setSystemTime(new Date(2022, 6, 20)); interaction.user.id = '123'; interaction.options.getString @@ -141,13 +142,13 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ + expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Verification failed check if you entered the correct information or contact admin.', - ephemeral: true, }); }); it('Status of study is not correct', async () => { + prismaMock.users.findMany.mockResolvedValue([]); dict['Status of studies as of 13/7/2022'] = 'Studies not in progress.'; interaction.user.id = '123'; interaction.options.getString @@ -170,16 +171,15 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ + expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Verification failed check if you entered the correct information or contact admin.', - ephemeral: true, }); }); - it('userLog file does not exist test should not fail', async () => { + it('User is already verified', async () => { + prismaMock.users.findMany.mockResolvedValue([databaseUser]); interaction.user.id = '123'; - fs.unlinkSync('./userLog.json'); interaction.options.getString .calledWith('linktoconfirmationmuni') .mockReturnValue( @@ -200,28 +200,52 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ - content: 'You have been successfully verified with role undefined.', - ephemeral: true, + expect(interaction.editReply).toHaveBeenCalledWith({ + content: + 'User already verified! Contact admin if you need to verify again.', }); }); - it('User is already verified', async () => { + it('User is using thesis which is assigned to different user', async () => { interaction.user.id = '123'; - let userLog = undefined; - try { - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } catch (e) { - fs.writeFileSync('./userLog.json', '[]'); - userLog = fs.readFileSync('./userLog.json', 'utf8'); + prismaMock.users.findMany + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + id: '1234', + discordId: '1234', + idThesis: '2223121', + status: 'verified', + }, + ]); + interaction.options.getString + .calledWith('linktoconfirmationmuni') + .mockReturnValue( + 'https://is.muni.cz/confirmation-of-studies/cccxxd3?lang=en' + ); + interaction.options.getString + .calledWith('bachelorthesislink') + .mockReturnValue('https://dspace.vutbr.cz/handle/11012/2223121'); + jest.spyOn(utils, 'scrapeThesis').mockReturnValue( + Promise.resolve(nameUser) + ); + jest.spyOn(utils, 'scrapeConfirmationStudies').mockReturnValue( + Promise.resolve(dict) + ); + if (interaction.guild) { + interaction.guild.roles.cache.find.mockReturnValue( + 'test' as unknown as Role + ); } - const userLogJSON = JSON.parse(userLog); - userLogJSON.push({ - id: '123', - idThesis: '222321', - status: 'verified', + await verify.execute(interaction); + expect(interaction.editReply).toHaveBeenCalledWith({ + content: 'This thesis is already used! Please contact admin.', }); - fs.writeFileSync('./userLog.json', JSON.stringify(userLogJSON)); + }); + + it('Both thesis and user is already verified', async () => { + interaction.user.id = '123'; + prismaMock.users.findMany.mockResolvedValue([databaseUser]); interaction.options.getString .calledWith('linktoconfirmationmuni') .mockReturnValue( @@ -242,29 +266,17 @@ describe('Tests for verify command', () => { ); } await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ + expect(interaction.editReply).toHaveBeenCalledWith({ content: 'User already verified! Contact admin if you need to verify again.', - ephemeral: true, }); }); - it('User is using thesis which is assigned to different user', async () => { + it('Database error when searching for users', async () => { + prismaMock.users.findMany.mockRejectedValue( + new Error('Connection error') + ); interaction.user.id = '123'; - let userLog = undefined; - try { - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } catch (e) { - fs.writeFileSync('./userLog.json', '[]'); - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } - const userLogJSON = JSON.parse(userLog); - userLogJSON.push({ - id: '1234', - idThesis: '2223121', - status: 'verified', - }); - fs.writeFileSync('./userLog.json', JSON.stringify(userLogJSON)); interaction.options.getString .calledWith('linktoconfirmationmuni') .mockReturnValue( @@ -284,29 +296,19 @@ describe('Tests for verify command', () => { 'test' as unknown as Role ); } + await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ - content: 'This thesis is already used! Please contact admin.', - ephemeral: true, + expect(interaction.editReply).toHaveBeenCalledWith({ + content: 'Verification failed! Contact admin.', }); }); - it('Both thesis and user is already verified', async () => { + it('Database error when creating user', async () => { + prismaMock.users.findMany.mockResolvedValue([]); + prismaMock.users.create.mockRejectedValue( + new Error('Connection error') + ); interaction.user.id = '123'; - let userLog = undefined; - try { - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } catch (e) { - fs.writeFileSync('./userLog.json', '[]'); - userLog = fs.readFileSync('./userLog.json', 'utf8'); - } - const userLogJSON = JSON.parse(userLog); - userLogJSON.push({ - id: '123', - idThesis: '2223121', - status: 'verified', - }); - fs.writeFileSync('./userLog.json', JSON.stringify(userLogJSON)); interaction.options.getString .calledWith('linktoconfirmationmuni') .mockReturnValue( @@ -326,11 +328,10 @@ describe('Tests for verify command', () => { 'test' as unknown as Role ); } + await verify.execute(interaction); - expect(interaction.reply).toHaveBeenCalledWith({ - content: - 'User already verified! Contact admin if you need to verify again.', - ephemeral: true, + expect(interaction.editReply).toHaveBeenCalledWith({ + content: 'Verification failed! Contact admin.', }); }); });