├── .gitignore ├── .env.example ├── .env.test.example ├── src ├── database.ts ├── server.ts ├── utils │ ├── emailUtils.ts │ ├── authUtils.ts │ └── errorsUtils.ts ├── controllers │ ├── userController.ts │ ├── authController.ts │ ├── coursesController.ts │ └── instructorsController.ts ├── schemas │ ├── authSchema.ts │ ├── userSchema.ts │ └── testSchema.ts ├── routes │ ├── userRouter.ts │ ├── index.ts │ ├── authRouter.ts │ ├── coursesRouter.ts │ └── instructorsRouter.ts ├── app.ts ├── middlewares │ ├── errorHandlingMiddleware.ts │ ├── validateSchemaMiddleware.ts │ └── validateTokenMiddleware.ts ├── repositories │ ├── userRepository.ts │ ├── coursesRepository.ts │ └── instructorsRepository.ts └── services │ ├── coursesService.ts │ ├── userService.ts │ ├── instructorsService.ts │ └── authService.ts ├── prisma ├── migrations │ ├── 20220423052723_fix_constrain_term_id_in_table_of_disciplines │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20220427152547_create_views_column_in_tests_table │ │ └── migration.sql │ ├── 20220502184151_update_constrains_on_the_table_users │ │ └── migration.sql │ ├── 20220424180937_remove_unique_of_term_id_and_discipline_id │ │ └── migration.sql │ ├── 20220424184723_remove_unique_of_table_tests_in_colums_category_id_and_teacher_discipline_id │ │ └── migration.sql │ ├── 20220422054313_update_table_of_session_adding_the_colum_of_tokens │ │ └── migration.sql │ ├── 20220420060218_create_database │ │ └── migration.sql │ ├── 20220420130432_create_tables_of_categories_terms_and_teachers │ │ └── migration.sql │ ├── 20220423050154_apply_constrain_in_table_sessions_token_is_unique │ │ └── migration.sql │ ├── 20220422053907_create_table_sessions │ │ └── migration.sql │ ├── 20220502175937_add_column_on_table_users │ │ └── migration.sql │ ├── 20220420130806_create_terms_and_teachers_tables │ │ └── migration.sql │ └── 20220420134909_finish_initial_database_configs │ │ └── migration.sql ├── schema.prisma └── seed.ts ├── tsconfig.json ├── tests ├── factories │ ├── instructorBodyFactory.ts │ ├── userBodyFactory.ts │ ├── diciplinesBodyFactory.ts │ ├── userFactory.ts │ ├── testBodyFactory.ts │ └── testFactory.ts └── app.test.ts ├── jest.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | .env.test -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | PORT= 3 | JWT_SECRET= 4 | CLIENT_ID= 5 | CLIENT_SECRET= 6 | SENDGRID_API_KEY= -------------------------------------------------------------------------------- /.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | PORT= 3 | JWT_SECRET= 4 | CLIENT_ID= 5 | CLIENT_SECRET= 6 | SENDGRID_API_KEY= -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import pkg from '@prisma/client'; 2 | 3 | const { PrismaClient } = pkg; 4 | export const prisma = new PrismaClient(); 5 | -------------------------------------------------------------------------------- /prisma/migrations/20220423052723_fix_constrain_term_id_in_table_of_disciplines/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "disciplines_termId_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/migrations/20220427152547_create_views_column_in_tests_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "tests" ADD COLUMN "views" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220502184151_update_constrains_on_the_table_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ALTER COLUMN "email" DROP NOT NULL, 3 | ALTER COLUMN "password" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app.js'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | app.listen(process.env.PORT, () => { 6 | console.log(`Server is running on PORT ${process.env.PORT}`); 7 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "outDir": "dist" 7 | }, 8 | "ts-node": { 9 | "esm": true 10 | } 11 | } -------------------------------------------------------------------------------- /prisma/migrations/20220424180937_remove_unique_of_term_id_and_discipline_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "teachersDisciplines_disciplineId_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "teachersDisciplines_teacherId_key"; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20220424184723_remove_unique_of_table_tests_in_colums_category_id_and_teacher_discipline_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "tests_categoryId_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "tests_teacherDisciplineId_key"; 6 | -------------------------------------------------------------------------------- /src/utils/emailUtils.ts: -------------------------------------------------------------------------------- 1 | export function generateEmailText( 2 | teacherName: string, 3 | categoryName: string, 4 | testName: string, 5 | disciplineName: string 6 | ) { 7 | return `A seguinte prova foi adicionada: ${teacherName} ${categoryName} ${testName} (${disciplineName})`; 8 | } -------------------------------------------------------------------------------- /tests/factories/instructorBodyFactory.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../../src/database.js'; 2 | import { Teacher } from '@prisma/client'; 3 | 4 | export default async function disciplineBodyFactory() { 5 | const teachers = await prisma.teacher.findMany(); 6 | const teacher: Teacher = teachers[0]; 7 | 8 | return teacher; 9 | } 10 | -------------------------------------------------------------------------------- /src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as userService from '../services/userService.js'; 3 | 4 | export async function register(req: Request, res: Response) { 5 | const registerData = req.body; 6 | 7 | await userService.create(registerData); 8 | 9 | res.sendStatus(201); 10 | } 11 | -------------------------------------------------------------------------------- /prisma/migrations/20220422054313_update_table_of_session_adding_the_colum_of_tokens/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `token` to the `sessions` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "sessions" ADD COLUMN "token" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /tests/factories/userBodyFactory.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { CreateUserData } from "../../src/services/userService.js"; 3 | 4 | export default function userBodyFactory(): CreateUserData { 5 | return { 6 | email: faker.internet.email(), 7 | password: faker.internet.password(), 8 | githubId: null 9 | }; 10 | } -------------------------------------------------------------------------------- /prisma/migrations/20220420060218_create_database/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" SERIAL NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "password" TEXT NOT NULL, 6 | 7 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 12 | -------------------------------------------------------------------------------- /tests/factories/diciplinesBodyFactory.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../../src/database.js'; 2 | import { Discipline } from '@prisma/client'; 3 | 4 | export default async function disciplineBodyFactory() { 5 | const disciplines = await prisma.discipline.findMany(); 6 | const discipline: Discipline = disciplines[0]; 7 | 8 | return discipline; 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | export default { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | extensionsToTreatAsEsm: [".ts"], 6 | globals: { 7 | "ts-jest": { 8 | useESM: true, 9 | }, 10 | }, 11 | moduleNameMapper: { 12 | "^(\\.{1,2}/.*)\\.js$": "$1", 13 | }, 14 | }; -------------------------------------------------------------------------------- /src/schemas/authSchema.ts: -------------------------------------------------------------------------------- 1 | import joi from 'joi'; 2 | import { User } from '@prisma/client'; 3 | 4 | type CreateAuthData = Omit; 5 | 6 | const authSchema = joi.object({ 7 | email: joi.string().email().required(), 8 | password: joi.string().required(), 9 | githubId: joi.allow(null) 10 | }); 11 | 12 | export default authSchema; -------------------------------------------------------------------------------- /src/schemas/userSchema.ts: -------------------------------------------------------------------------------- 1 | import joi from 'joi'; 2 | import { User } from '@prisma/client'; 3 | 4 | type CreateUserData = Omit; 5 | 6 | const userSchema = joi.object({ 7 | email: joi.string().email().required(), 8 | password: joi.string().required(), 9 | githubId: joi.allow(null) 10 | }); 11 | 12 | export default userSchema; -------------------------------------------------------------------------------- /prisma/migrations/20220420130432_create_tables_of_categories_terms_and_teachers/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "categories" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | 6 | CONSTRAINT "categories_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- CreateIndex 10 | CREATE UNIQUE INDEX "categories_name_key" ON "categories"("name"); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20220423050154_apply_constrain_in_table_sessions_token_is_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[token]` on the table `sessions` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token"); 9 | -------------------------------------------------------------------------------- /src/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as userController from "../controllers/userController.js"; 3 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 4 | 5 | const userRouter = Router(); 6 | 7 | userRouter.post('/register', validateSchemaMiddleware, userController.register); 8 | 9 | export default userRouter; -------------------------------------------------------------------------------- /src/schemas/testSchema.ts: -------------------------------------------------------------------------------- 1 | import joi from 'joi'; 2 | 3 | const testSchema = joi.object({ 4 | name: joi.string().required(), 5 | pdfUrl: joi.string().uri().required(), 6 | categoryId: joi.number().required(), 7 | views: joi.number().required(), 8 | disciplineId: joi.number().required(), 9 | teacherId: joi.number().required() 10 | }); 11 | 12 | export default testSchema; -------------------------------------------------------------------------------- /tests/factories/userFactory.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import { prisma } from "../../src/database.js"; 3 | import { CreateUserData } from "../../src/services/userService.js"; 4 | 5 | export default async function userFactory(user: CreateUserData) { 6 | await prisma.user.create({ 7 | data: { 8 | ...user, 9 | password: bcrypt.hashSync(user.password, 10), 10 | }, 11 | }); 12 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { json } from 'express'; 2 | import 'express-async-errors'; 3 | import cors from 'cors'; 4 | import router from './routes/index.js'; 5 | import errorHandlingMiddleware from './middlewares/errorHandlingMiddleware.js'; 6 | 7 | const app = express(); 8 | 9 | app.use(cors()); 10 | app.use(json()); 11 | app.use(router); 12 | app.use(errorHandlingMiddleware); 13 | 14 | export default app; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20220422053907_create_table_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "sessions" ( 3 | "id" SERIAL NOT NULL, 4 | "userId" INTEGER NOT NULL, 5 | 6 | CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- AddForeignKey 10 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20220502175937_add_column_on_table_users/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[githubId]` on the table `users` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "users" ADD COLUMN "githubId" INTEGER; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "users_githubId_key" ON "users"("githubId"); 12 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import authRouter from './authRouter.js'; 3 | import coursesRouter from './coursesRouter.js'; 4 | import instructorsRouter from './instructorsRouter.js'; 5 | import userRouter from './userRouter.js'; 6 | 7 | const router = Router(); 8 | 9 | router.use(authRouter); 10 | router.use(userRouter); 11 | router.use(coursesRouter); 12 | router.use(instructorsRouter); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/routes/authRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as authController from "../controllers/authController.js"; 3 | import validateSchemaMiddleware from "../middlewares/validateSchemaMiddleware.js"; 4 | 5 | const authRouter = Router(); 6 | 7 | authRouter.post('/login', validateSchemaMiddleware, authController.login); 8 | authRouter.post('/login/oauth/github', authController.loginGitHub); 9 | authRouter.delete('/logout/:userId', authController.logout); 10 | 11 | export default authRouter; -------------------------------------------------------------------------------- /src/middlewares/errorHandlingMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export default function errorHandlingMiddleware(error, req: Request, res: Response, next: NextFunction) { 4 | console.log(error); 5 | if (error.type === 'error_bad_request') return res.sendStatus(400); 6 | if (error.type === 'error_unauthorized') return res.sendStatus(401); 7 | if (error.type === 'error_not_found') return res.sendStatus(404); 8 | if (error.type === 'error_conflict') return res.sendStatus(409); 9 | 10 | res.sendStatus(500); 11 | } 12 | -------------------------------------------------------------------------------- /src/repositories/userRepository.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../database.js'; 2 | import { User } from '@prisma/client'; 3 | import userRouter from '../routes/userRouter.js'; 4 | 5 | type CreateUserData = Omit; 6 | 7 | export async function create(userData: CreateUserData) { 8 | await prisma.user.create({ data: userData }); 9 | } 10 | 11 | export async function update(userData: CreateUserData) { 12 | await prisma.user.update({ 13 | where: { 14 | email: userData.email 15 | }, 16 | data: { 17 | password: userData.password 18 | }, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /prisma/migrations/20220420130806_create_terms_and_teachers_tables/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "terms" ( 3 | "id" SERIAL NOT NULL, 4 | "number" INTEGER NOT NULL, 5 | 6 | CONSTRAINT "terms_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- CreateTable 10 | CREATE TABLE "teachers" ( 11 | "id" SERIAL NOT NULL, 12 | "name" TEXT NOT NULL, 13 | 14 | CONSTRAINT "teachers_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "terms_number_key" ON "terms"("number"); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "teachers_name_key" ON "teachers"("name"); 22 | -------------------------------------------------------------------------------- /src/routes/coursesRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as coursesController from '../controllers/coursesController.js'; 3 | import validateTokenMiddleware from '../middlewares/validateTokenMiddleware.js'; 4 | 5 | const coursesRouter = Router(); 6 | 7 | coursesRouter.use(validateTokenMiddleware); 8 | 9 | coursesRouter.get('/disciplines', coursesController.getDisciplines); 10 | coursesRouter.get('/disciplines/:disciplineName', coursesController.getDisciplinesByName); 11 | coursesRouter.put('/disciplines/tests/:testId', coursesController.updateTestViewsById); 12 | 13 | export default coursesRouter; 14 | -------------------------------------------------------------------------------- /tests/factories/testBodyFactory.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../../src/database.js'; 2 | import { faker } from '@faker-js/faker'; 3 | import { Category, teachersDisciplines } from '@prisma/client'; 4 | 5 | interface CreateTestData { 6 | name: String; 7 | pdfUrl: String; 8 | categoryId: Number; 9 | disciplineId: Number; 10 | teacherId: Number; 11 | views: Number; 12 | }; 13 | 14 | export default async function testBodyFactory(disciplineId: number, teacherId: number) { 15 | const categories = await prisma.category.findMany(); 16 | const category: Category = categories[0]; 17 | 18 | const test: CreateTestData = { 19 | name: faker.lorem.lines(), 20 | pdfUrl: faker.internet.url(), 21 | categoryId: category.id, 22 | disciplineId, 23 | teacherId, 24 | views: 0, 25 | }; 26 | 27 | return test; 28 | } 29 | -------------------------------------------------------------------------------- /src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as authService from '../services/authService.js'; 3 | 4 | export async function login(req: Request, res: Response) { 5 | const loginData = req.body; 6 | 7 | const session = await authService.create(loginData); 8 | 9 | res.status(200).send(session); 10 | } 11 | 12 | export async function loginGitHub(req: Request, res: Response) { 13 | const loginGitHubData: authService.CreateLoginGitHubData = req.body; 14 | 15 | const session = await authService.createGitHub(loginGitHubData); 16 | 17 | res.status(200).send(session); 18 | } 19 | 20 | export async function logout(req: Request, res: Response) { 21 | const userId = parseInt(req.params.userId); 22 | 23 | await authService.remove(userId); 24 | 25 | res.sendStatus(200); 26 | } 27 | -------------------------------------------------------------------------------- /src/controllers/coursesController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as coursesService from '../services/coursesService.js'; 3 | 4 | export async function getDisciplines(req: Request, res: Response) { 5 | const disciplines = await coursesService.getDisciplines(); 6 | 7 | res.send(disciplines); 8 | } 9 | 10 | export async function getDisciplinesByName(req: Request, res: Response) { 11 | const disciplineName: string = req.params.disciplineName; 12 | 13 | const termsByDisciplines = await coursesService.getDisciplinesByName(disciplineName); 14 | 15 | res.send(termsByDisciplines); 16 | } 17 | 18 | export async function updateTestViewsById(req: Request, res: Response) { 19 | const testId: number = parseInt(req.params.testId); 20 | 21 | await coursesService.updateTestViewsById(testId); 22 | 23 | res.sendStatus(200); 24 | } -------------------------------------------------------------------------------- /tests/factories/testFactory.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../../src/database.js'; 2 | import { faker } from "@faker-js/faker"; 3 | import { Test, Category, teachersDisciplines } from '@prisma/client'; 4 | 5 | export default async function testFactory(disciplineId: number, teacherId: number) { 6 | const teacherDiscipline: teachersDisciplines = await prisma.teachersDisciplines.create({ 7 | data: { 8 | teacherId: disciplineId, 9 | disciplineId: teacherId 10 | } 11 | }); 12 | 13 | const categories = await prisma.category.findMany(); 14 | const category: Category = categories[0]; 15 | 16 | const test: Test = await prisma.test.create({ 17 | data: { 18 | name: faker.lorem.lines(), 19 | pdfUrl: faker.internet.url(), 20 | categoryId: category.id, 21 | teacherDisciplineId: teacherDiscipline.id, 22 | views: 0 23 | } 24 | }); 25 | 26 | return test; 27 | } 28 | -------------------------------------------------------------------------------- /src/services/coursesService.ts: -------------------------------------------------------------------------------- 1 | import * as coursesRepository from '../repositories/coursesRepository.js'; 2 | import * as errorsUtils from '../utils/errorsUtils.js'; 3 | 4 | export async function getDisciplines() { 5 | const disciplines = await coursesRepository.getAllDisciplines(); 6 | 7 | return disciplines; 8 | } 9 | 10 | export async function getDisciplinesByName(disciplineName: string) { 11 | const discipline = await coursesRepository.getDisciplineByName(disciplineName); 12 | if(!discipline) throw errorsUtils.notFoundError('Discipline Name'); 13 | 14 | const disciplines = await coursesRepository.getDisciplinesByName(disciplineName); 15 | 16 | return disciplines; 17 | } 18 | 19 | export async function updateTestViewsById(testId: number) { 20 | const test = await coursesRepository.getTest(testId); 21 | if(!test) throw errorsUtils.notFoundError('Test'); 22 | 23 | await coursesRepository.updateTestViewsById(testId); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/authUtils.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | export async function loginGitHub(code: string) { 6 | const headers = { "headers": { "Accept": "application/json" } }; 7 | 8 | const params = new URLSearchParams({ 9 | client_id: process.env.CLIENT_ID, 10 | client_secret: process.env.CLIENT_SECRET, 11 | code: code, 12 | }); 13 | 14 | const URL_CONSTRUCTOR = "https://github.com/login/oauth/access_token?" + params.toString() 15 | const promise = await axios.post(URL_CONSTRUCTOR, null, headers); 16 | 17 | return promise.data; 18 | } 19 | 20 | export async function getUserDataGitHub(token: string, tokenType: string) { 21 | const headers = { "headers": { "Authorization": `${tokenType} ${token}` } }; 22 | 23 | const URL_CONSTRUCTOR = "https://api.github.com/user"; 24 | const promise = await axios.get(URL_CONSTRUCTOR, headers); 25 | 26 | return promise.data; 27 | } -------------------------------------------------------------------------------- /src/utils/errorsUtils.ts: -------------------------------------------------------------------------------- 1 | export function notFoundError(entity: string) { 2 | return { 3 | type: 'error_not_found', 4 | message: `Could not find specified "${entity}"!` 5 | }; 6 | } 7 | 8 | export function badRequestError(entity: string) { 9 | return { 10 | type: 'error_bad_request', 11 | message: entity ? `This is a "${entity}" Error!` : `That's an error!` 12 | }; 13 | } 14 | 15 | export function unauthorizedError(entity: string) { 16 | return { 17 | type: 'error_unauthorized', 18 | message: entity 19 | ? `To access your account you need to be authenticated, enter the correct "${entity}"!` 20 | : `To access, you must be authenticated!` 21 | }; 22 | } 23 | 24 | export function conflictError(entity: string) { 25 | return { 26 | type: 'error_conflict', 27 | message: entity 28 | ? `A conflict has been generated with the "${entity}", please try again!` 29 | : `A conflict has been generated, please try again!` 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/services/userService.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../database.js'; 2 | import { User } from '@prisma/client'; 3 | import * as errorsUtils from '../utils/errorsUtils.js'; 4 | import * as userRepository from '../repositories/userRepository.js'; 5 | import bcrypt from 'bcrypt'; 6 | 7 | export type CreateUserData = Omit; 8 | 9 | export async function create(registerData: CreateUserData) { 10 | const searchedUser = await prisma.user.findFirst({ where: { email: registerData.email } }); 11 | if (searchedUser && !searchedUser.githubId) throw errorsUtils.conflictError('User'); 12 | if (searchedUser?.githubId && searchedUser.password) throw errorsUtils.conflictError('User'); 13 | 14 | const passwordHash = bcrypt.hashSync(registerData.password, 10); 15 | const userData = { ...registerData, password: passwordHash }; 16 | 17 | if (searchedUser?.githubId) { 18 | userRepository.update(userData); 19 | } else { 20 | userRepository.create(userData); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/middlewares/validateSchemaMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { stripHtml } from 'string-strip-html'; 3 | import authSchema from '../schemas/authSchema.js'; 4 | import testSchema from '../schemas/testSchema.js'; 5 | import userSchema from '../schemas/userSchema.js'; 6 | 7 | function sanitizeString(string: string) { 8 | return stripHtml(string).result.trim(); 9 | } 10 | 11 | const schemas = { 12 | '/login': authSchema, 13 | '/register': userSchema, 14 | '/instructors/tests/create': testSchema 15 | }; 16 | 17 | export default async function validateSchemaMiddleware(req: Request, res: Response, next: NextFunction) { 18 | const { body } = req; 19 | 20 | let schema; 21 | if (req.path.includes('instructors')) { 22 | schema = schemas['/instructors/tests/create']; 23 | } else { 24 | schema = schemas['/' + req.path.split('/')[1]]; 25 | } 26 | 27 | Object.keys(body).forEach((key) => { 28 | if (typeof body[key] === 'string') body[key] = sanitizeString(body[key]); 29 | }); 30 | 31 | const validation = schema.validate(body, { abortEarly: false }); 32 | if (validation.error) return res.status(422).send(validation.error.message); 33 | 34 | next(); 35 | } 36 | -------------------------------------------------------------------------------- /src/routes/instructorsRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as instructorsController from '../controllers/instructorsController.js'; 3 | import validateSchemaMiddleware from '../middlewares/validateSchemaMiddleware.js'; 4 | import validateTokenMiddleware from '../middlewares/validateTokenMiddleware.js'; 5 | 6 | const instructorsRouter = Router(); 7 | 8 | instructorsRouter.use(validateTokenMiddleware); 9 | 10 | instructorsRouter.get('/instructors/disciplines/:disciplineId', instructorsController.getInstructorsByDiscipline); 11 | instructorsRouter.get('/instructors/disciplines', instructorsController.getAllDisciplines); 12 | instructorsRouter.get('/instructors', instructorsController.getInstructors); 13 | instructorsRouter.get('/instructors/categories', instructorsController.getInstructorsCategories); 14 | instructorsRouter.get('/instructors/:instructorName', instructorsController.getInstructorsByName); 15 | instructorsRouter.post( 16 | '/instructors/tests/create', 17 | validateSchemaMiddleware, 18 | instructorsController.createTestByInstructor 19 | ); 20 | instructorsRouter.put('/instructors/tests/:testId', instructorsController.updateTestViewsById); 21 | 22 | export default instructorsRouter; 23 | -------------------------------------------------------------------------------- /src/middlewares/validateTokenMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { prisma } from "../database.js"; 3 | import * as errorsUtils from "../utils/errorsUtils.js"; 4 | import jwt from 'jsonwebtoken'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | export default async function validateTokenMiddleware(req:Request, res:Response, next:NextFunction) { 9 | const authorization = req.headers.authorization; 10 | const token = authorization?.replace('Bearer ', ''); 11 | const secretKey = process.env.JWT_SECRET; 12 | if(!token) throw errorsUtils.unauthorizedError(''); 13 | 14 | try { 15 | const session = await prisma.sessions.findUnique({ where: { token: token } }); 16 | if(!session) throw errorsUtils.unauthorizedError(''); 17 | 18 | try { 19 | const user = await prisma.user.findUnique({ where: { id: session.userId } }); 20 | if(!user) throw errorsUtils.notFoundError('User'); 21 | 22 | jwt.verify(session.token, secretKey); 23 | 24 | res.locals.user = {email: user.email, userId: session.userId}; 25 | 26 | next(); 27 | } catch (error) { 28 | console.log(error); 29 | res.sendStatus(401); 30 | } 31 | } catch (error) { 32 | console.log(error); 33 | res.sendStatus(500); 34 | } 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-repo-provas", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "dotenv -e .env.test prisma db seed && NODE_OPTIONS=--experimental-vm-modules dotenv -e .env.test jest -i", 9 | "start": "node src/server.ts", 10 | "dev": "nodemon src/server.ts", 11 | "dev:test": "dotenv -e .env.test nodemon src/server.ts", 12 | "migrate": "prisma migrate dev", 13 | "migrate:test": "dotenv -e .env.test prisma migrate dev" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@prisma/client": "^3.12.0", 20 | "axios": "^0.27.2", 21 | "bcrypt": "^5.0.1", 22 | "cors": "^2.8.5", 23 | "dotenv": "^16.0.0", 24 | "dotenv-cli": "^5.1.0", 25 | "express": "^4.17.3", 26 | "express-async-errors": "^3.1.1", 27 | "joi": "^17.6.0", 28 | "jsonwebtoken": "^8.5.1", 29 | "pg": "^8.7.3", 30 | "prisma": "^3.12.0", 31 | "string-strip-html": "^9.1.11" 32 | }, 33 | "devDependencies": { 34 | "@faker-js/faker": "^6.2.0", 35 | "@sendgrid/mail": "^7.6.2", 36 | "@types/bcrypt": "^5.0.0", 37 | "@types/cors": "^2.8.12", 38 | "@types/express": "^4.17.13", 39 | "@types/jest": "^27.4.1", 40 | "@types/jsonwebtoken": "^8.5.8", 41 | "@types/node": "^17.0.25", 42 | "@types/pg": "^8.6.5", 43 | "@types/supertest": "^2.0.12", 44 | "jest": "^27.5.1", 45 | "nodemon": "^2.0.15", 46 | "supertest": "^6.2.3", 47 | "ts-jest": "^27.1.4", 48 | "ts-node": "^10.7.0", 49 | "typescript": "^4.6.3" 50 | }, 51 | "prisma": { 52 | "seed": "ts-node prisma/seed.ts" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/repositories/coursesRepository.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../database.js'; 2 | 3 | export async function getAllDisciplines() { 4 | const terms = await prisma.term.findMany({ 5 | select: { 6 | id: true, 7 | number: true, 8 | discipline: { 9 | select: { 10 | id: true, 11 | name: true, 12 | teachersDisciplines: { 13 | include: { 14 | teacher: true, 15 | test: { 16 | include: { 17 | category: true 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }); 26 | 27 | return terms; 28 | } 29 | 30 | export async function getDisciplineByName(disciplineName: string) { 31 | const discipline = await prisma.discipline.findFirst({ 32 | where: { 33 | name: disciplineName 34 | } 35 | }); 36 | 37 | return discipline; 38 | } 39 | 40 | export async function getDisciplinesByName(disciplineName: string) { 41 | const termsByDisciplines = await prisma.term.findMany({ 42 | select: { 43 | id: true, 44 | number: true, 45 | discipline: { 46 | select: { 47 | id: true, 48 | name: true, 49 | teachersDisciplines: { 50 | include: { 51 | teacher: true, 52 | test: { 53 | include: { 54 | category: true 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | where: { 61 | name: { 62 | contains: disciplineName, 63 | mode: 'insensitive' 64 | } 65 | } 66 | } 67 | } 68 | }); 69 | 70 | return termsByDisciplines; 71 | } 72 | 73 | export async function getTest(testId: number) { 74 | const test = await prisma.test.findFirst({ 75 | where: { 76 | id: testId 77 | } 78 | }); 79 | 80 | return test; 81 | } 82 | 83 | export async function updateTestViewsById(testId: number) { 84 | await prisma.test.update({ 85 | where: { 86 | id: testId 87 | }, 88 | data: { 89 | views: { 90 | increment: 1 91 | } 92 | } 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/controllers/instructorsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as instructorsService from '../services/instructorsService.js'; 3 | 4 | export async function getInstructors(req: Request, res: Response) { 5 | const instructors = await instructorsService.getInstructors(); 6 | 7 | res.send(instructors); 8 | } 9 | 10 | export async function getInstructorsCategories(req: Request, res: Response) { 11 | const categories = await instructorsService.getInstructorsCategories(); 12 | 13 | res.send(categories); 14 | } 15 | 16 | export async function getInstructorsByName(req: Request, res: Response) { 17 | const instructorName: string = req.params.instructorName; 18 | 19 | const teachersByName = await instructorsService.getInstructorsByName(instructorName); 20 | 21 | res.send(teachersByName); 22 | } 23 | 24 | export async function updateTestViewsById(req: Request, res: Response) { 25 | const testId: number = parseInt(req.params.testId); 26 | 27 | await instructorsService.updateTestViewsById(testId); 28 | 29 | res.sendStatus(200); 30 | } 31 | 32 | export async function getAllDisciplines(req: Request, res: Response) { 33 | const disciplines = await instructorsService.getAllDisciplines(); 34 | 35 | res.send(disciplines); 36 | } 37 | 38 | export async function getInstructorsByDiscipline(req: Request, res: Response) { 39 | const disciplineId: number = parseInt(req.params.disciplineId); 40 | 41 | const teachersByDiscipline = await instructorsService.getInstructorsByDiscipline(disciplineId); 42 | 43 | res.send(teachersByDiscipline); 44 | } 45 | 46 | export async function createTestByInstructor(req: Request, res: Response) { 47 | const testData = req.body; 48 | const newTeacherDisciplineData: instructorsService.CreateNewTeacherDisciplineData = { 49 | disciplineId: testData.disciplineId, 50 | teacherId: testData.teacherId 51 | }; 52 | const newTestData: instructorsService.CreateNewTestData = { 53 | name: testData.name, 54 | pdfUrl: testData.pdfUrl, 55 | categoryId: testData.categoryId, 56 | views: testData.views 57 | }; 58 | 59 | await instructorsService.createTestByInstructor(newTeacherDisciplineData, newTestData); 60 | 61 | res.sendStatus(201); 62 | } 63 | -------------------------------------------------------------------------------- /prisma/migrations/20220420134909_finish_initial_database_configs/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "disciplines" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "termId" INTEGER NOT NULL, 6 | 7 | CONSTRAINT "disciplines_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "teachersDisciplines" ( 12 | "id" SERIAL NOT NULL, 13 | "teacherId" INTEGER NOT NULL, 14 | "disciplineId" INTEGER NOT NULL, 15 | 16 | CONSTRAINT "teachersDisciplines_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateTable 20 | CREATE TABLE "tests" ( 21 | "id" SERIAL NOT NULL, 22 | "name" TEXT NOT NULL, 23 | "pdfUrl" TEXT NOT NULL, 24 | "categoryId" INTEGER NOT NULL, 25 | "teacherDisciplineId" INTEGER NOT NULL, 26 | 27 | CONSTRAINT "tests_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateIndex 31 | CREATE UNIQUE INDEX "disciplines_name_key" ON "disciplines"("name"); 32 | 33 | -- CreateIndex 34 | CREATE UNIQUE INDEX "disciplines_termId_key" ON "disciplines"("termId"); 35 | 36 | -- CreateIndex 37 | CREATE UNIQUE INDEX "teachersDisciplines_teacherId_key" ON "teachersDisciplines"("teacherId"); 38 | 39 | -- CreateIndex 40 | CREATE UNIQUE INDEX "teachersDisciplines_disciplineId_key" ON "teachersDisciplines"("disciplineId"); 41 | 42 | -- CreateIndex 43 | CREATE UNIQUE INDEX "tests_categoryId_key" ON "tests"("categoryId"); 44 | 45 | -- CreateIndex 46 | CREATE UNIQUE INDEX "tests_teacherDisciplineId_key" ON "tests"("teacherDisciplineId"); 47 | 48 | -- AddForeignKey 49 | ALTER TABLE "disciplines" ADD CONSTRAINT "disciplines_termId_fkey" FOREIGN KEY ("termId") REFERENCES "terms"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 50 | 51 | -- AddForeignKey 52 | ALTER TABLE "teachersDisciplines" ADD CONSTRAINT "teachersDisciplines_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "teachers"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 53 | 54 | -- AddForeignKey 55 | ALTER TABLE "teachersDisciplines" ADD CONSTRAINT "teachersDisciplines_disciplineId_fkey" FOREIGN KEY ("disciplineId") REFERENCES "disciplines"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "tests" ADD CONSTRAINT "tests_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "tests" ADD CONSTRAINT "tests_teacherDisciplineId_fkey" FOREIGN KEY ("teacherDisciplineId") REFERENCES "teachersDisciplines"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | email String? @unique 16 | password String? 17 | githubId Int? @unique 18 | sessions sessions[] 19 | 20 | @@map("users") 21 | } 22 | 23 | model Category { 24 | id Int @id @default(autoincrement()) 25 | name String @unique 26 | test Test[] 27 | 28 | @@map("categories") 29 | } 30 | 31 | model Term { 32 | id Int @id @default(autoincrement()) 33 | number Int @unique 34 | discipline Discipline[] 35 | 36 | @@map("terms") 37 | } 38 | 39 | model Teacher { 40 | id Int @id @default(autoincrement()) 41 | name String @unique 42 | teachersDisciplines teachersDisciplines[] 43 | 44 | @@map("teachers") 45 | } 46 | 47 | model Discipline { 48 | id Int @id @default(autoincrement()) 49 | name String @unique 50 | term Term @relation(fields: [termId], references: [id]) 51 | termId Int 52 | teachersDisciplines teachersDisciplines[] 53 | 54 | @@map("disciplines") 55 | } 56 | 57 | model teachersDisciplines { 58 | id Int @id @default(autoincrement()) 59 | teacher Teacher @relation(fields: [teacherId], references: [id]) 60 | teacherId Int 61 | discipline Discipline @relation(fields: [disciplineId], references: [id]) 62 | disciplineId Int 63 | test Test[] 64 | } 65 | 66 | model Test { 67 | id Int @id @default(autoincrement()) 68 | name String 69 | pdfUrl String 70 | category Category @relation(fields: [categoryId], references: [id]) 71 | categoryId Int 72 | teachersDisciplines teachersDisciplines @relation(fields: [teacherDisciplineId], references: [id]) 73 | teacherDisciplineId Int 74 | views Int @default(0) 75 | 76 | @@map("tests") 77 | } 78 | 79 | model sessions { 80 | id Int @id @default(autoincrement()) 81 | user User @relation(fields: [userId], references: [id]) 82 | userId Int 83 | token String @unique 84 | } -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../src/database.js'; 2 | import { User, Term, Discipline, Category, Teacher } from '@prisma/client'; 3 | 4 | type CreateUserData = Omit; 5 | type CreateTermData = Omit; 6 | type CreateDisciplineData = Omit; 7 | type CreateCategoryData = Omit; 8 | type CreateTeacherData = Omit; 9 | 10 | async function main() { 11 | const defaultUser : CreateUserData = { 12 | email: 'ruineto@email.com', 13 | password: '1234', 14 | githubId: null 15 | }; 16 | 17 | const defaultTerms: CreateTermData[] = [ 18 | { number: 1 }, 19 | { number: 2 }, 20 | { number: 3 }, 21 | { number: 4 }, 22 | { number: 5 }, 23 | { number: 6 } 24 | ]; 25 | 26 | const defaultDisciplines: CreateDisciplineData[] = [ 27 | { name: 'HTML', termId: 1 }, 28 | { name: 'CSS', termId: 1 }, 29 | { name: 'JavaScript', termId: 2 }, 30 | { name: 'Axios', termId: 2 }, 31 | { name: 'React', termId: 3 }, 32 | { name: 'StyledComponents', termId: 3 }, 33 | { name: 'Node', termId: 4 }, 34 | { name: 'Express', termId: 4 }, 35 | { name: 'NPM', termId: 4 }, 36 | { name: 'Mongo', termId: 5 }, 37 | { name: 'SQL', termId: 6 } 38 | ]; 39 | 40 | const defaultCategories: CreateCategoryData[] = [ { name: 'P1' }, { name: 'P2' }, { name: 'P3' } ]; 41 | 42 | const defaultTeachers: CreateTeacherData[] = [ 43 | { name: 'Fulano' }, 44 | { name: 'Ciclano' }, 45 | { name: 'Beltrano' }, 46 | { name: 'Fulana' }, 47 | { name: 'Ciclana' }, 48 | { name: 'Beltrana' } 49 | ]; 50 | 51 | await prisma.user.upsert({ 52 | where: { email: defaultUser.email }, 53 | update: {}, 54 | create: { ...defaultUser } 55 | }); 56 | 57 | for (let i = 0; i < defaultTerms.length; i++) { 58 | const term = defaultTerms[i]; 59 | 60 | await prisma.term.upsert({ 61 | where: { number: term.number }, 62 | update: {}, 63 | create: { ...term } 64 | }); 65 | } 66 | 67 | for (let i = 0; i < defaultDisciplines.length; i++) { 68 | const discipline = defaultDisciplines[i]; 69 | 70 | await prisma.discipline.upsert({ 71 | where: { name: discipline.name }, 72 | update: {}, 73 | create: { ...discipline } 74 | }); 75 | } 76 | 77 | for (let i = 0; i < defaultCategories.length; i++) { 78 | const category = defaultCategories[i]; 79 | 80 | await prisma.category.upsert({ 81 | where: { name: category.name }, 82 | update: {}, 83 | create: { ...category } 84 | }); 85 | } 86 | 87 | for (let i = 0; i < defaultTeachers.length; i++) { 88 | const teacher = defaultTeachers[i]; 89 | 90 | await prisma.teacher.upsert({ 91 | where: { name: teacher.name }, 92 | update: {}, 93 | create: { ...teacher } 94 | }); 95 | } 96 | } 97 | 98 | main() 99 | .catch((e) => { 100 | console.log(e); 101 | process.exit(1); 102 | }) 103 | .finally(async () => { 104 | await prisma.$disconnect(); 105 | }); 106 | -------------------------------------------------------------------------------- /src/services/instructorsService.ts: -------------------------------------------------------------------------------- 1 | import * as instructorsRepository from '../repositories/instructorsRepository.js'; 2 | import * as errorsUtils from '../utils/errorsUtils.js'; 3 | import * as emailUtils from '../utils/emailUtils.js'; 4 | import sgMail from '@sendgrid/mail'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 9 | 10 | export type CreateNewTeacherDisciplineData = Omit; 11 | export type CreateNewTestData = Omit, 'teacherDisciplineId'>; 12 | 13 | export async function getInstructors() { 14 | const instructors = await instructorsRepository.getInstructors(); 15 | 16 | return instructors; 17 | } 18 | 19 | export async function getInstructorsCategories() { 20 | const categories = await instructorsRepository.getAllCategories(); 21 | 22 | return categories; 23 | } 24 | 25 | export async function getInstructorsByName(instructorName: string) { 26 | const instructor = await instructorsRepository.getInstructorByName(instructorName); 27 | if(!instructor) throw errorsUtils.notFoundError('Instructor Name'); 28 | 29 | const instructors = await instructorsRepository.getInstructorsByName(instructorName); 30 | 31 | return instructors; 32 | } 33 | 34 | export async function updateTestViewsById(testId: number) { 35 | const test = await instructorsRepository.getTest(testId); 36 | if(!test) throw errorsUtils.notFoundError('Test'); 37 | 38 | await instructorsRepository.updateTestViewsById(testId); 39 | } 40 | 41 | export async function getAllDisciplines() { 42 | const disciplines = await instructorsRepository.getAllDisciplines(); 43 | 44 | return disciplines; 45 | } 46 | 47 | export async function getInstructorsByDiscipline(disciplineId: number) { 48 | const instructorsByDiscipline = await instructorsRepository.getInstructorsByDiscipline(disciplineId); 49 | 50 | return instructorsByDiscipline; 51 | } 52 | 53 | export async function createTestByInstructor( 54 | teachersDisciplinesData: CreateNewTeacherDisciplineData, 55 | testData: CreateNewTestData 56 | ) { 57 | const teacherDisciplineByData = await instructorsRepository.createTeachersDisciplines(teachersDisciplinesData); 58 | 59 | await instructorsRepository.createTest({ 60 | ...testData, 61 | teacherDisciplineId: teacherDisciplineByData.id 62 | }); 63 | 64 | const emails = await instructorsRepository.getAllEmails(); 65 | const teacher = await instructorsRepository.findTeacherById(teachersDisciplinesData.teacherId); 66 | const discipline = await instructorsRepository.findDisciplineById(teachersDisciplinesData.disciplineId); 67 | const category = await instructorsRepository.findCategoryById(testData.categoryId); 68 | const emailsArray = emails.map(user => user.email); 69 | const msg = { 70 | to: emailsArray, 71 | from: 'ruineto11@gmail.com', 72 | subject: 'RepoProvas - Nova Prova Adicionada', 73 | text: emailUtils.generateEmailText(teacher.name, category.name, testData.name, discipline.name) 74 | }; 75 | 76 | try { 77 | await sgMail.sendMultiple(msg); 78 | 79 | console.log('Emails sent!'); 80 | } catch (error) { 81 | console.error(error); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/services/authService.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client'; 2 | import { prisma } from '../database.js'; 3 | import * as errorsUtils from '../utils/errorsUtils.js'; 4 | import * as authUtils from '../utils/authUtils.js'; 5 | import bcrypt from 'bcrypt'; 6 | import jwt from 'jsonwebtoken'; 7 | import dotenv from 'dotenv'; 8 | dotenv.config(); 9 | 10 | type CreateLoginData = Omit; 11 | 12 | export interface CreateLoginGitHubData { 13 | code: string; 14 | } 15 | 16 | export async function create(loginData: CreateLoginData) { 17 | const secretKey = process.env.JWT_SECRET; 18 | const configuration = { expiresIn: 60 * 60 }; 19 | 20 | const user = await prisma.user.findUnique({ where: { email: loginData.email } }); 21 | if (!user) throw errorsUtils.unauthorizedError('Credentials'); 22 | 23 | const isAuthorized = bcrypt.compareSync(loginData.password, user.password); 24 | if (isAuthorized) { 25 | const token = jwt.sign(loginData, secretKey, configuration); 26 | 27 | await prisma.sessions.create({ data: { token, userId: user.id } }); 28 | 29 | return { token, userId: user.id, email: user.email }; 30 | } 31 | 32 | throw errorsUtils.unauthorizedError('Credentials'); 33 | } 34 | 35 | export async function createGitHub(data: CreateLoginGitHubData) { 36 | const secretKey = process.env.JWT_SECRET; 37 | const configuration = { expiresIn: 60 * 60 }; 38 | 39 | const dataToken = await authUtils.loginGitHub(data.code); 40 | 41 | const userDataGitHub = await authUtils.getUserDataGitHub( 42 | dataToken.access_token, 43 | dataToken.token_type 44 | ); 45 | 46 | const userGitHub = await prisma.user.findUnique({ 47 | where: { 48 | githubId: userDataGitHub.id 49 | } 50 | }); 51 | 52 | if (!userGitHub) { 53 | if (!userDataGitHub.email) { 54 | const user = await prisma.user.create({ 55 | data: { 56 | githubId: userDataGitHub.id 57 | } 58 | }); 59 | 60 | delete user.password; 61 | const token = jwt.sign(user, secretKey, configuration); 62 | 63 | await prisma.sessions.create({ data: { token, userId: user.githubId } }); 64 | 65 | return { token, userId: user.githubId, email: user.email }; 66 | } 67 | 68 | const user = await prisma.user.upsert({ 69 | where: { 70 | email: userDataGitHub.email 71 | }, 72 | update: { 73 | email: userDataGitHub.email, 74 | githubId: userDataGitHub.id 75 | }, 76 | create: { 77 | email: userDataGitHub.email, 78 | githubId: userDataGitHub.id 79 | } 80 | }); 81 | 82 | delete user.password; 83 | const token = jwt.sign(user, secretKey, configuration); 84 | 85 | await prisma.sessions.create({ data: { token, userId: user.id } }); 86 | 87 | return { token, userId: user.id, email: user.email }; 88 | } 89 | 90 | delete userGitHub.password; 91 | const token = jwt.sign(userGitHub, secretKey, configuration); 92 | 93 | await prisma.sessions.create({ data: { token, userId: userGitHub.id } }); 94 | 95 | return { token, userId: userGitHub.id, email: userGitHub.email }; 96 | } 97 | 98 | export async function remove(userId: number) { 99 | const user = await prisma.user.findUnique({ where: { id: userId } }); 100 | if (!user) throw errorsUtils.notFoundError('User'); 101 | 102 | const session = await prisma.sessions.findFirst({ where: { userId: user.id } }); 103 | if (!session) throw errorsUtils.badRequestError('Session'); 104 | 105 | await prisma.sessions.delete({ where: { token: session.token } }); 106 | } 107 | -------------------------------------------------------------------------------- /src/repositories/instructorsRepository.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../database.js'; 2 | import { teachersDisciplines, Test } from '@prisma/client'; 3 | 4 | export type CreateTeacherDisciplineData = Omit; 5 | export type CreateTestData = Omit; 6 | 7 | export async function getInstructors() { 8 | const teachers = await prisma.teacher.findMany({ 9 | select: { 10 | id: true, 11 | name: true, 12 | teachersDisciplines: { 13 | include: { 14 | discipline: true, 15 | test: { 16 | include: { 17 | category: true 18 | } 19 | } 20 | } 21 | } 22 | } 23 | }); 24 | 25 | return teachers; 26 | } 27 | 28 | export async function getInstructorByName(instructorName: string) { 29 | const instructor = await prisma.teacher.findFirst({ 30 | where: { 31 | name: instructorName 32 | } 33 | }); 34 | 35 | return instructor; 36 | } 37 | 38 | export async function getInstructorsByName(instructorName: string) { 39 | const teachersByName = await prisma.teacher.findMany({ 40 | select: { 41 | id: true, 42 | name: true, 43 | teachersDisciplines: { 44 | include: { 45 | discipline: true, 46 | test: { 47 | include: { 48 | category: true 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | where: { 55 | name: { 56 | contains: instructorName, 57 | mode: 'insensitive' 58 | } 59 | } 60 | }); 61 | 62 | return teachersByName; 63 | } 64 | 65 | export async function getAllCategories() { 66 | const categories = await prisma.category.findMany(); 67 | 68 | return categories; 69 | } 70 | 71 | export async function getTest(testId: number) { 72 | const test = await prisma.test.findFirst({ 73 | where: { 74 | id: testId 75 | } 76 | }); 77 | 78 | return test; 79 | } 80 | 81 | export async function updateTestViewsById(testId: number) { 82 | await prisma.test.update({ 83 | where: { 84 | id: testId 85 | }, 86 | data: { 87 | views: { 88 | increment: 1 89 | } 90 | } 91 | }); 92 | } 93 | 94 | export async function getAllDisciplines() { 95 | const disciplines = await prisma.discipline.findMany(); 96 | 97 | return disciplines; 98 | } 99 | 100 | export async function getInstructorsByDiscipline(disciplineId: number) { 101 | const instructorsByDiscipline = await prisma.teacher.findMany({ 102 | include: { 103 | teachersDisciplines: { 104 | include: { 105 | discipline: true 106 | }, 107 | where: { 108 | disciplineId: disciplineId 109 | } 110 | } 111 | } 112 | }); 113 | 114 | return instructorsByDiscipline; 115 | } 116 | 117 | export async function createTeachersDisciplines(teacherDisciplineData: CreateTeacherDisciplineData) { 118 | const teacherDiscipline = await prisma.teachersDisciplines.create({ 119 | data: { 120 | teacherId: teacherDisciplineData.teacherId, 121 | disciplineId: teacherDisciplineData.disciplineId 122 | } 123 | }); 124 | 125 | return teacherDiscipline; 126 | } 127 | 128 | export async function createTest(testData: CreateTestData) { 129 | await prisma.test.create({ 130 | data: { 131 | name: testData.name, 132 | pdfUrl: testData.pdfUrl, 133 | views: testData.views, 134 | categoryId: testData.categoryId, 135 | teacherDisciplineId: testData.teacherDisciplineId 136 | } 137 | }); 138 | } 139 | 140 | export async function getAllEmails() { 141 | const emails = await prisma.user.findMany({ 142 | select: { 143 | email: true 144 | } 145 | }); 146 | 147 | return emails; 148 | } 149 | 150 | export async function findTeacherById(teacherId: number) { 151 | const teacher = await prisma.teacher.findUnique({ 152 | where: { 153 | id: teacherId 154 | } 155 | }); 156 | 157 | return teacher; 158 | } 159 | 160 | export async function findDisciplineById(disciplineId: number) { 161 | const discipline = await prisma.discipline.findUnique({ 162 | where: { 163 | id: disciplineId 164 | } 165 | }); 166 | 167 | return discipline; 168 | } 169 | 170 | export async function findCategoryById(categoryId: number) { 171 | const category = await prisma.category.findUnique({ 172 | where: { 173 | id: categoryId 174 | } 175 | }) 176 | 177 | return category; 178 | } -------------------------------------------------------------------------------- /tests/app.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../src/app.js'; 2 | import supertest from 'supertest'; 3 | import { prisma } from '../src/database.js'; 4 | import { faker } from '@faker-js/faker'; 5 | import { jest } from '@jest/globals'; 6 | import userFactory from './factories/userFactory.js'; 7 | import userBodyFactory from './factories/userBodyFactory.js'; 8 | import diciplinesBodyFactory from './factories/diciplinesBodyFactory.js'; 9 | import instructorBodyFactory from './factories/instructorBodyFactory.js'; 10 | import testFactory from './factories/testFactory.js'; 11 | import testBodyFactory from './factories/testBodyFactory.js'; 12 | 13 | describe('User tests: POST /register', () => { 14 | beforeEach(truncateUsers); 15 | 16 | afterAll(disconnect); 17 | 18 | it('should return 201 and persist the user given a valid body', async () => { 19 | const body = userBodyFactory(); 20 | 21 | const result = await supertest(app).post('/register').send(body); 22 | const user = await prisma.user.findUnique({ 23 | where: { 24 | email: body.email 25 | } 26 | }); 27 | 28 | expect(result.status).toEqual(201); 29 | expect(user).not.toBeNull(); 30 | }); 31 | 32 | it('should return 422 given a invalid body', async () => { 33 | const body = {}; 34 | 35 | const result = await supertest(app).post('/register').send(body); 36 | expect(result.status).toEqual(422); 37 | }); 38 | 39 | it('should return 409 given a duplicate email', async () => { 40 | const body = userBodyFactory(); 41 | 42 | await supertest(app).post('/register').send(body); 43 | const result = await supertest(app).post('/register').send(body); 44 | const users = await prisma.user.findMany({ 45 | where: { 46 | email: body.email 47 | } 48 | }); 49 | 50 | expect(result.status).toEqual(409); 51 | expect(users.length).toEqual(1); 52 | }); 53 | }); 54 | 55 | describe('User tests: POST /login', () => { 56 | beforeEach(truncateUsers); 57 | 58 | afterAll(disconnect); 59 | 60 | it('should return 200 and a token given valid credentials', async () => { 61 | const body = userBodyFactory(); 62 | await userFactory(body); 63 | 64 | const response = await supertest(app).post('/login').send(body); 65 | 66 | expect(response.status).toEqual(200); 67 | expect(typeof response.body.token).toEqual('string'); 68 | expect(response.body.token.length).toBeGreaterThan(0); 69 | }); 70 | 71 | it('should return 401 given invalid email', async () => { 72 | const body = userBodyFactory(); 73 | 74 | const response = await supertest(app).post('/login').send(body); 75 | 76 | expect(response.status).toEqual(401); 77 | }); 78 | 79 | it('should return 401 given invalid password', async () => { 80 | const body = userBodyFactory(); 81 | await userFactory(body); 82 | 83 | const response = await supertest(app).post('/login').send({ 84 | ...body, 85 | password: faker.internet.password() 86 | }); 87 | 88 | expect(response.status).toEqual(401); 89 | }); 90 | }); 91 | 92 | describe('Discipline search tests: GET /disciplines/:displineName', () => { 93 | beforeEach(truncateUsers); 94 | 95 | afterAll(disconnect); 96 | 97 | it('should return 401 given invalid token', async () => { 98 | const displineName = faker.name.firstName(); 99 | 100 | const response = await supertest(app).get(`/disciplines/${displineName}`); 101 | 102 | expect(response.status).toEqual(401); 103 | }); 104 | 105 | it('should return 200 and a token given valid credentials', async () => { 106 | const dicipline = await diciplinesBodyFactory(); 107 | const user = userBodyFactory(); 108 | await userFactory(user); 109 | 110 | const response = await supertest(app).post('/login').send(user); 111 | 112 | expect(response.status).toEqual(200); 113 | expect(typeof response.body.token).toEqual('string'); 114 | expect(response.body.token.length).toBeGreaterThan(0); 115 | 116 | const result = await supertest(app) 117 | .get(`/disciplines/${dicipline.name}`) 118 | .set('Authorization', `Bearer ${response.body.token}`); 119 | 120 | expect(result.status).toEqual(200); 121 | }); 122 | 123 | it('should return 404 given invalid discipline name', async () => { 124 | const diciplineName = faker.name.firstName(); 125 | const user = userBodyFactory(); 126 | await userFactory(user); 127 | 128 | const response = await supertest(app).post('/login').send(user); 129 | 130 | expect(response.status).toEqual(200); 131 | expect(typeof response.body.token).toEqual('string'); 132 | expect(response.body.token.length).toBeGreaterThan(0); 133 | 134 | const result = await supertest(app) 135 | .get(`/disciplines/${diciplineName}`) 136 | .set('Authorization', `Bearer ${response.body.token}`); 137 | 138 | expect(result.status).toEqual(404); 139 | }); 140 | 141 | it('should return 200 and a token given valid discipline name', async () => { 142 | const dicipline = await diciplinesBodyFactory(); 143 | const user = userBodyFactory(); 144 | await userFactory(user); 145 | 146 | const response = await supertest(app).post('/login').send(user); 147 | 148 | expect(response.status).toEqual(200); 149 | expect(typeof response.body.token).toEqual('string'); 150 | expect(response.body.token.length).toBeGreaterThan(0); 151 | 152 | const result = await supertest(app) 153 | .get(`/disciplines/${dicipline.name}`) 154 | .set('Authorization', `Bearer ${response.body.token}`); 155 | 156 | expect(result.status).toEqual(200); 157 | expect(result.body.length).toBeGreaterThan(0); 158 | }); 159 | }); 160 | 161 | describe('Instructor search tests: GET /instructors/:instructorName', () => { 162 | beforeEach(truncateUsers); 163 | 164 | afterAll(disconnect); 165 | 166 | it('should return 401 given invalid token', async () => { 167 | const instructorName = faker.name.firstName(); 168 | 169 | const response = await supertest(app).get(`/instructors/${instructorName}`); 170 | 171 | expect(response.status).toEqual(401); 172 | }); 173 | 174 | it('should return 200 and a token given valid credentials', async () => { 175 | const instructor = await instructorBodyFactory(); 176 | const user = userBodyFactory(); 177 | await userFactory(user); 178 | 179 | const response = await supertest(app).post('/login').send(user); 180 | 181 | expect(response.status).toEqual(200); 182 | expect(typeof response.body.token).toEqual('string'); 183 | expect(response.body.token.length).toBeGreaterThan(0); 184 | 185 | const result = await supertest(app) 186 | .get(`/instructors/${instructor.name}`) 187 | .set('Authorization', `Bearer ${response.body.token}`); 188 | 189 | expect(result.status).toEqual(200); 190 | }); 191 | 192 | it('should return 404 given invalid instructor name', async () => { 193 | const instructorName = faker.name.firstName(); 194 | const user = userBodyFactory(); 195 | await userFactory(user); 196 | 197 | const response = await supertest(app).post('/login').send(user); 198 | 199 | expect(response.status).toEqual(200); 200 | expect(typeof response.body.token).toEqual('string'); 201 | expect(response.body.token.length).toBeGreaterThan(0); 202 | 203 | const result = await supertest(app) 204 | .get(`/instructors/${instructorName}`) 205 | .set('Authorization', `Bearer ${response.body.token}`); 206 | 207 | expect(result.status).toEqual(404); 208 | }); 209 | 210 | it('should return 200 and a token given valid instructor name', async () => { 211 | const instructor = await instructorBodyFactory(); 212 | const user = userBodyFactory(); 213 | await userFactory(user); 214 | 215 | const response = await supertest(app).post('/login').send(user); 216 | 217 | expect(response.status).toEqual(200); 218 | expect(typeof response.body.token).toEqual('string'); 219 | expect(response.body.token.length).toBeGreaterThan(0); 220 | 221 | const result = await supertest(app) 222 | .get(`/instructors/${instructor.name}`) 223 | .set('Authorization', `Bearer ${response.body.token}`); 224 | 225 | expect(result.status).toEqual(200); 226 | expect(result.body.length).toBeGreaterThan(0); 227 | }); 228 | }); 229 | 230 | describe('Test disciplines update views tests: PUT /disciplines/tests/:testId', () => { 231 | beforeEach(truncateUsers); 232 | beforeEach(truncateTests); 233 | 234 | afterAll(disconnect); 235 | 236 | it('should return 401 given invalid token', async () => { 237 | const testId = 1; 238 | 239 | const response = await supertest(app).get(`/instructors/${testId}`); 240 | 241 | expect(response.status).toEqual(401); 242 | }); 243 | 244 | it('should return 200 and a token given valid credentials', async () => { 245 | const discipline = await diciplinesBodyFactory(); 246 | const instructor = await instructorBodyFactory(); 247 | const test = await testFactory(discipline.id, instructor.id); 248 | const user = userBodyFactory(); 249 | await userFactory(user); 250 | 251 | const response = await supertest(app).post('/login').send(user); 252 | 253 | expect(response.status).toEqual(200); 254 | expect(typeof response.body.token).toEqual('string'); 255 | expect(response.body.token.length).toBeGreaterThan(0); 256 | 257 | const result = await supertest(app) 258 | .put(`/disciplines/tests/${test.id}`) 259 | .set('Authorization', `Bearer ${response.body.token}`); 260 | 261 | expect(result.status).toEqual(200); 262 | }); 263 | 264 | it('should return 404 given invalid test id', async () => { 265 | const testId = 999; 266 | const user = userBodyFactory(); 267 | await userFactory(user); 268 | 269 | const response = await supertest(app).post('/login').send(user); 270 | 271 | expect(response.status).toEqual(200); 272 | expect(typeof response.body.token).toEqual('string'); 273 | expect(response.body.token.length).toBeGreaterThan(0); 274 | 275 | const result = await supertest(app) 276 | .put(`/disciplines/tests/${testId}`) 277 | .set('Authorization', `Bearer ${response.body.token}`); 278 | 279 | expect(result.status).toEqual(404); 280 | }); 281 | 282 | it('should return 200 and a token given valid test id', async () => { 283 | const discipline = await diciplinesBodyFactory(); 284 | const instructor = await instructorBodyFactory(); 285 | const test = await testFactory(discipline.id, instructor.id); 286 | const user = userBodyFactory(); 287 | await userFactory(user); 288 | 289 | const response = await supertest(app).post('/login').send(user); 290 | 291 | expect(response.status).toEqual(200); 292 | expect(typeof response.body.token).toEqual('string'); 293 | expect(response.body.token.length).toBeGreaterThan(0); 294 | 295 | const currentTest = await prisma.test.findUnique({ 296 | where: { 297 | id: test.id 298 | } 299 | }); 300 | 301 | const result = await supertest(app) 302 | .put(`/disciplines/tests/${test.id}`) 303 | .set('Authorization', `Bearer ${response.body.token}`); 304 | 305 | const updatedTest = await prisma.test.findUnique({ 306 | where: { 307 | id: test.id 308 | } 309 | }); 310 | 311 | expect(result.status).toEqual(200); 312 | expect(updatedTest.views).toEqual(currentTest.views + 1); 313 | }); 314 | }); 315 | 316 | describe('Test instructors update views tests: PUT /instructors/tests/:testId', () => { 317 | beforeEach(truncateUsers); 318 | beforeEach(truncateTests); 319 | 320 | afterAll(disconnect); 321 | 322 | it('should return 401 given invalid token', async () => { 323 | const testId = 12; 324 | 325 | const response = await supertest(app).get(`/instructors/tests/${testId}`); 326 | 327 | expect(response.status).toEqual(401); 328 | }); 329 | 330 | it('should return 200 and a token given valid credentials', async () => { 331 | const discipline = await diciplinesBodyFactory(); 332 | const instructor = await instructorBodyFactory(); 333 | const test = await testFactory(discipline.id, instructor.id); 334 | const user = userBodyFactory(); 335 | await userFactory(user); 336 | 337 | const response = await supertest(app).post('/login').send(user); 338 | 339 | expect(response.status).toEqual(200); 340 | expect(typeof response.body.token).toEqual('string'); 341 | expect(response.body.token.length).toBeGreaterThan(0); 342 | 343 | const result = await supertest(app) 344 | .put(`/instructors/tests/${test.id}`) 345 | .set('Authorization', `Bearer ${response.body.token}`); 346 | 347 | expect(result.status).toEqual(200); 348 | }); 349 | 350 | it('should return 404 given invalid test id', async () => { 351 | const testId = 99999; 352 | const user = userBodyFactory(); 353 | await userFactory(user); 354 | 355 | const response = await supertest(app).post('/login').send(user); 356 | 357 | expect(response.status).toEqual(200); 358 | expect(typeof response.body.token).toEqual('string'); 359 | expect(response.body.token.length).toBeGreaterThan(0); 360 | 361 | const result = await supertest(app) 362 | .put(`/instructors/tests/${testId}`) 363 | .set('Authorization', `Bearer ${response.body.token}`); 364 | 365 | expect(result.status).toEqual(404); 366 | }); 367 | 368 | it('should return 200 and a token given valid test id', async () => { 369 | const discipline = await diciplinesBodyFactory(); 370 | const instructor = await instructorBodyFactory(); 371 | const test = await testFactory(discipline.id, instructor.id); 372 | const user = userBodyFactory(); 373 | await userFactory(user); 374 | 375 | const response = await supertest(app).post('/login').send(user); 376 | 377 | expect(response.status).toEqual(200); 378 | expect(typeof response.body.token).toEqual('string'); 379 | expect(response.body.token.length).toBeGreaterThan(0); 380 | 381 | const currentTest = await prisma.test.findUnique({ 382 | where: { 383 | id: test.id 384 | } 385 | }); 386 | 387 | const result = await supertest(app) 388 | .put(`/instructors/tests/${test.id}`) 389 | .set('Authorization', `Bearer ${response.body.token}`); 390 | 391 | const updatedTest = await prisma.test.findUnique({ 392 | where: { 393 | id: test.id 394 | } 395 | }); 396 | 397 | expect(result.status).toEqual(200); 398 | expect(updatedTest.views).toEqual(currentTest.views + 1); 399 | }); 400 | }); 401 | 402 | describe('Test create tests: POST /instructors/tests/create', () => { 403 | beforeEach(truncateUsers); 404 | beforeEach(truncateTests); 405 | 406 | afterAll(disconnect); 407 | 408 | it('should return 401 given invalid token', async () => { 409 | const response = await supertest(app).post(`/instructors/tests/create`); 410 | 411 | expect(response.status).toEqual(401); 412 | }); 413 | 414 | it('should return 201 and a token given valid credentials', async () => { 415 | const discipline = await diciplinesBodyFactory(); 416 | const instructor = await instructorBodyFactory(); 417 | const test = await testBodyFactory(discipline.id, instructor.id); 418 | const user = userBodyFactory(); 419 | await userFactory(user); 420 | 421 | const logSpy = jest.spyOn(console, 'log'); 422 | 423 | const response = await supertest(app).post('/login').send(user); 424 | 425 | expect(response.status).toEqual(200); 426 | expect(typeof response.body.token).toEqual('string'); 427 | expect(response.body.token.length).toBeGreaterThan(0); 428 | 429 | const result = await supertest(app) 430 | .post('/instructors/tests/create') 431 | .send(test) 432 | .set('Authorization', `Bearer ${response.body.token}`); 433 | 434 | expect(result.status).toEqual(201); 435 | 436 | console.log('Emails sent!'); 437 | 438 | expect(logSpy).toHaveBeenCalledWith('Emails sent!'); 439 | }); 440 | 441 | it('should return 422 given a invalid body', async () => { 442 | const test = {}; 443 | const user = userBodyFactory(); 444 | await userFactory(user); 445 | 446 | const response = await supertest(app).post('/login').send(user); 447 | 448 | expect(response.status).toEqual(200); 449 | expect(typeof response.body.token).toEqual('string'); 450 | expect(response.body.token.length).toBeGreaterThan(0); 451 | 452 | const result = await supertest(app) 453 | .post('/instructors/tests/create') 454 | .send(test) 455 | .set('Authorization', `Bearer ${response.body.token}`); 456 | 457 | expect(result.status).toEqual(422); 458 | }); 459 | 460 | it('should return 201 and a token given valid body', async () => { 461 | const discipline = await diciplinesBodyFactory(); 462 | const instructor = await instructorBodyFactory(); 463 | const test = await testBodyFactory(discipline.id, instructor.id); 464 | const user = userBodyFactory(); 465 | await userFactory(user); 466 | 467 | const logSpy = jest.spyOn(console, 'log'); 468 | 469 | const response = await supertest(app).post('/login').send(user); 470 | 471 | expect(response.status).toEqual(200); 472 | expect(typeof response.body.token).toEqual('string'); 473 | expect(response.body.token.length).toBeGreaterThan(0); 474 | 475 | const result = await supertest(app) 476 | .post('/instructors/tests/create') 477 | .send(test) 478 | .set('Authorization', `Bearer ${response.body.token}`); 479 | 480 | expect(result.status).toEqual(201); 481 | 482 | console.log('Emails sent!'); 483 | 484 | expect(logSpy).toHaveBeenCalledWith('Emails sent!'); 485 | }); 486 | }); 487 | 488 | async function disconnect() { 489 | await prisma.$disconnect(); 490 | } 491 | 492 | async function truncateUsers() { 493 | await prisma.$executeRaw`TRUNCATE TABLE users, sessions;`; 494 | } 495 | 496 | async function truncateTests() { 497 | await prisma.$executeRaw`TRUNCATE TABLE tests, "teachersDisciplines";`; 498 | } 499 | --------------------------------------------------------------------------------