diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 5e9e3b624..ca3a25e92 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -31,4 +31,5 @@ Please describe how you tested this PR (both manually and with tests) Provide in - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes +- [ ] I've run the end to end tests - [ ] Any dependent changes have been merged and published in downstream modules diff --git a/packages/api-v2/ormconfig.ts b/packages/api-v2/ormconfig.ts index bfe899ba8..35867bc72 100644 --- a/packages/api-v2/ormconfig.ts +++ b/packages/api-v2/ormconfig.ts @@ -12,7 +12,7 @@ const ormconfig: TypeOrmModuleOptions = { username: process.env.POSTGRES_USERNAME, password: process.env.POSTGRES_PASSWORD || "", database: process.env.POSTGRES_DATABASE, - synchronize: false, + synchronize: process.env.NODE_ENV === "testing", entities: [Student, Plan], migrations: ["./dist/migrations/*.js"], cli: { diff --git a/packages/api-v2/package.json b/packages/api-v2/package.json index c154a914a..66f22a62d 100644 --- a/packages/api-v2/package.json +++ b/packages/api-v2/package.json @@ -21,7 +21,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:db:up": "docker-compose -f ../../infrastructure/test/docker-compose.e2e.yml up -d", "test:db:down": "docker-compose -f ../../infrastructure/test/docker-compose.e2e.yml down", - "test:e2e": "yarn g:cross-env NODE_ENV=testing jest --config ./test/jest-e2e.json --detectOpenHandles", + "test:e2e": "yarn g:cross-env NODE_ENV=testing jest --config ./test/jest-e2e.json --detectOpenHandles --no-cache --force-exit", "typeorm": "yarn g:cross-env NODE_ENV=development ts-node --project ./tsconfig.json ../../node_modules/typeorm/cli.js", "dev:db:reset": "yarn typeorm schema:drop", "dev:migration:generate": "yarn dev:db:reset && yarn typeorm migration:run && yarn typeorm migration:generate", diff --git a/packages/api-v2/src/auth/auth.controller.ts b/packages/api-v2/src/auth/auth.controller.ts index 94529a55d..fbc8f6825 100644 --- a/packages/api-v2/src/auth/auth.controller.ts +++ b/packages/api-v2/src/auth/auth.controller.ts @@ -66,9 +66,11 @@ export class AuthController { sameSite: "strict", secure: isSecure, }); - await this.emailConfirmationService.sendVerificationLink( - createStudentDto.email - ); + if (process.env.NODE_ENV !== "testing") { + await this.emailConfirmationService.sendVerificationLink( + createStudentDto.email + ); + } return student; } diff --git a/packages/api-v2/src/auth/auth.errors.ts b/packages/api-v2/src/auth/auth.errors.ts index 980909b80..e04b6d9c2 100644 --- a/packages/api-v2/src/auth/auth.errors.ts +++ b/packages/api-v2/src/auth/auth.errors.ts @@ -14,4 +14,4 @@ export class TokenExpiredError extends Error { constructor() { super(); } -} \ No newline at end of file +} diff --git a/packages/api-v2/src/auth/interfaces/authenticated-request.ts b/packages/api-v2/src/auth/interfaces/authenticated-request.ts index 31dff0b48..76b7c3b20 100644 --- a/packages/api-v2/src/auth/interfaces/authenticated-request.ts +++ b/packages/api-v2/src/auth/interfaces/authenticated-request.ts @@ -1,8 +1,6 @@ import { Student } from "src/student/entities/student.entity"; -/** - * Represents an authenticated request using the JwtAuthGuard. - */ +/** Represents an authenticated request using the JwtAuthGuard. */ export interface AuthenticatedRequest extends Request { user: Student; } diff --git a/packages/api-v2/src/auth/interfaces/jwt-payload.ts b/packages/api-v2/src/auth/interfaces/jwt-payload.ts index 5b02e89e8..655d791be 100644 --- a/packages/api-v2/src/auth/interfaces/jwt-payload.ts +++ b/packages/api-v2/src/auth/interfaces/jwt-payload.ts @@ -1,6 +1,4 @@ -/** - * Represents the information stored in the JWT. - */ +/** Represents the information stored in the JWT. */ export interface JwtPayload { uuid: string; email: string; diff --git a/packages/api-v2/src/environment-variables.ts b/packages/api-v2/src/environment-variables.ts index ec1510506..969750bb8 100644 --- a/packages/api-v2/src/environment-variables.ts +++ b/packages/api-v2/src/environment-variables.ts @@ -12,5 +12,5 @@ export interface EnvironmentVariables { EMAIL_SERVICE: string; EMAIL_USER: string; EMAIL_PASSWORD: string; - FORGOT_PASSWORD_URL: string + FORGOT_PASSWORD_URL: string; } diff --git a/packages/api-v2/src/guards/dev-route.guard.ts b/packages/api-v2/src/guards/dev-route.guard.ts index 999ab48ef..4e714eb91 100644 --- a/packages/api-v2/src/guards/dev-route.guard.ts +++ b/packages/api-v2/src/guards/dev-route.guard.ts @@ -1,9 +1,7 @@ import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; import { Observable } from "rxjs"; -/** - * Guards dev routes so that they are not accessible in production. - */ +/** Guards dev routes so that they are not accessible in production. */ @Injectable() export class DevRouteGuard implements CanActivate { canActivate( diff --git a/packages/api-v2/src/guards/jwt-auth.guard.ts b/packages/api-v2/src/guards/jwt-auth.guard.ts index a37276087..d47604158 100644 --- a/packages/api-v2/src/guards/jwt-auth.guard.ts +++ b/packages/api-v2/src/guards/jwt-auth.guard.ts @@ -2,9 +2,9 @@ import { Injectable } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; /** - * Used to protect any route with the JWT strategy. - * The JWT strategy verifies a JWT in the request and attaches the student to - * the request object on successful verfication. + * Used to protect any route with the JWT strategy. The JWT strategy verifies a + * JWT in the request and attaches the student to the request object on + * successful verfication. */ @Injectable() export class JwtAuthGuard extends AuthGuard("jwt") {} diff --git a/packages/api-v2/src/student/student.controller.ts b/packages/api-v2/src/student/student.controller.ts index 3fb0a4fcb..b43b965c4 100644 --- a/packages/api-v2/src/student/student.controller.ts +++ b/packages/api-v2/src/student/student.controller.ts @@ -30,11 +30,15 @@ import { ChangePasswordDto, wrongPasswordError, } from "@graduate/common"; -import { EmailAlreadyExists, WeakPassword, WrongPassword } from "./student.errors"; +import { + EmailAlreadyExists, + WeakPassword, + WrongPassword, +} from "./student.errors"; @Controller("students") export class StudentController { - constructor(private readonly studentService: StudentService) { } + constructor(private readonly studentService: StudentService) {} @UseGuards(JwtAuthGuard) @Get("me") @@ -123,12 +127,15 @@ export class StudentController { @Req() req: AuthenticatedRequest, @Body() changePasswordDto: ChangePasswordDto ): Promise { - const changePasswordResult = await this.studentService.changePassword(req.user.uuid, changePasswordDto); + const changePasswordResult = await this.studentService.changePassword( + req.user.uuid, + changePasswordDto + ); if (changePasswordResult instanceof WrongPassword) { throw new BadRequestException(wrongPasswordError); } if (changePasswordResult instanceof WeakPassword) { - throw new BadRequestException(weakPasswordError) + throw new BadRequestException(weakPasswordError); } } diff --git a/packages/api-v2/src/student/student.errors.ts b/packages/api-v2/src/student/student.errors.ts index 54bce6345..35b23c408 100644 --- a/packages/api-v2/src/student/student.errors.ts +++ b/packages/api-v2/src/student/student.errors.ts @@ -32,4 +32,4 @@ export class EmailNotConfirmed extends Error { constructor() { super(); } -} \ No newline at end of file +} diff --git a/packages/api-v2/src/student/student.service.ts b/packages/api-v2/src/student/student.service.ts index 0da0199c6..0edddaa39 100644 --- a/packages/api-v2/src/student/student.service.ts +++ b/packages/api-v2/src/student/student.service.ts @@ -16,7 +16,12 @@ import { UpdateStudentDto, } from "@graduate/common"; import { Student } from "./entities/student.entity"; -import { EmailAlreadyExists, NewPasswordsDontMatch, WeakPassword, WrongPassword } from "./student.errors"; +import { + EmailAlreadyExists, + NewPasswordsDontMatch, + WeakPassword, + WrongPassword, +} from "./student.errors"; @Injectable() export class StudentService { @@ -25,7 +30,7 @@ export class StudentService { constructor( @InjectRepository(Student) private studentRepository: Repository - ) { } + ) {} async create( createStudentDto: SignUpStudentDto @@ -161,8 +166,12 @@ export class StudentService { return formatServiceCtx(StudentService.name, methodName); } - async changePassword(uuid: any, changePasswordDto: ChangePasswordDto): Promise { - const { currentPassword, newPassword, newPasswordConfirm } = changePasswordDto; + async changePassword( + uuid: any, + changePasswordDto: ChangePasswordDto + ): Promise { + const { currentPassword, newPassword, newPasswordConfirm } = + changePasswordDto; const student = await this.findByUuid(uuid); if (newPassword !== newPasswordConfirm) { @@ -170,40 +179,50 @@ export class StudentService { } const { password: trueHashedPassword } = student; - const isValidPassword = await bcrypt.compare(currentPassword, trueHashedPassword); + const isValidPassword = await bcrypt.compare( + currentPassword, + trueHashedPassword + ); if (!isValidPassword) { - this.logger.debug( - { message: "Invalid password", oldPassword: currentPassword }, - ); + this.logger.debug({ + message: "Invalid password", + oldPassword: currentPassword, + }); return new WrongPassword(); } if (!isStrongPassword(newPassword)) { - this.logger.debug( - { message: "weak password", oldPassword: currentPassword }, - ); + this.logger.debug({ + message: "weak password", + oldPassword: currentPassword, + }); return new WeakPassword(); } - await this.studentRepository.save(Object.assign(student, { password: newPassword })); + await this.studentRepository.save( + Object.assign(student, { password: newPassword }) + ); } - async resetPassword(email, resetPasswordData: ResetPasswordDto): Promise { + async resetPassword( + email, + resetPasswordData: ResetPasswordDto + ): Promise { const { password, passwordConfirm } = resetPasswordData; - const student = await this.findByEmail(email) + const student = await this.findByEmail(email); if (password !== passwordConfirm) { return new NewPasswordsDontMatch(); } if (!isStrongPassword(password)) { - this.logger.debug( - { message: "weak password", password }, - ); + this.logger.debug({ message: "weak password", password }); return new WeakPassword(); } - return await this.studentRepository.save(Object.assign(student, { password })) + return await this.studentRepository.save( + Object.assign(student, { password }) + ); } } diff --git a/packages/api-v2/test/auth/auth.e2e-spec.ts b/packages/api-v2/test/auth/auth.e2e-spec.ts index 420861628..1d6b055bb 100644 --- a/packages/api-v2/test/auth/auth.e2e-spec.ts +++ b/packages/api-v2/test/auth/auth.e2e-spec.ts @@ -9,7 +9,7 @@ describe("AuthController (e2e)", () => { let app: INestApplication; let connection: Connection; - beforeEach(async () => { + beforeAll(async () => { app = await initializeApp(); connection = app.get(Connection); @@ -30,7 +30,8 @@ describe("AuthController (e2e)", () => { .post("/auth/register") .send({ email: "test-register@gmail.com", - password: "1234567890", + password: "1234567890a", + passwordConfirm: "1234567890a", }) .expect(201); @@ -50,6 +51,11 @@ describe("AuthController (e2e)", () => { .into(Student) .values([{ ...testUser1 }]); + await request(app.getHttpServer()) + .post("/auth/register") + .send(testUser1) + .expect(201); + await request(app.getHttpServer()) .post("/auth/register") .send(testUser1) @@ -57,6 +63,11 @@ describe("AuthController (e2e)", () => { }); it("logs in with valid credentials", async () => { + await request(app.getHttpServer()) + .post("/auth/register") + .send(testUser1) + .expect(201); + const res = await request(app.getHttpServer()) .post("/auth/login") .send(testUser1) diff --git a/packages/api-v2/test/jest-e2e.json b/packages/api-v2/test/jest-e2e.json index a483ce748..143782d47 100644 --- a/packages/api-v2/test/jest-e2e.json +++ b/packages/api-v2/test/jest-e2e.json @@ -6,5 +6,8 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "moduleNameMapper": { + "^src/(.*)": "/../src/$1" + }, "transformIgnorePatterns": ["^.+\\.js$"] } diff --git a/packages/api-v2/test/major/major.e2e-spec.ts b/packages/api-v2/test/major/major.e2e-spec.ts index 8a97b4c82..3efc89b7c 100644 --- a/packages/api-v2/test/major/major.e2e-spec.ts +++ b/packages/api-v2/test/major/major.e2e-spec.ts @@ -15,7 +15,7 @@ describe("MajorController (e2e)", () => { it("retrieves the major for a valid year and major name", async () => { const response = await request(app.getHttpServer()) - .post("/majors/2022/Computer%20Science,%20BSCS") + .get("/majors/2022/Computer%20Science,%20BSCS") .expect(200); const major = response.body; @@ -25,14 +25,14 @@ describe("MajorController (e2e)", () => { it("fails to retrieve a major from an invalid year", () => { const INVALID_YEAR = 2000; return request(app.getHttpServer()) - .post(`/majors/${INVALID_YEAR}/Computer%20Science,%20BSCS`) - .expect(400); + .get(`/majors/${INVALID_YEAR}/Computer%20Science,%20BSCS`) + .expect(404); }); it("fails to retrieve a major from a valid year but invalid major name", () => { const INVALID_MAJOR_NAME = "wrong"; return request(app.getHttpServer()) - .post(`/majors/2022/${INVALID_MAJOR_NAME}`) - .expect(400); + .get(`/majors/2022/${INVALID_MAJOR_NAME}`) + .expect(404); }); }); diff --git a/packages/api-v2/test/plan/plan.e2e-spec.ts b/packages/api-v2/test/plan/plan.e2e-spec.ts index 95324d2df..06fe9c545 100644 --- a/packages/api-v2/test/plan/plan.e2e-spec.ts +++ b/packages/api-v2/test/plan/plan.e2e-spec.ts @@ -3,7 +3,12 @@ import { Plan } from "../../src/plan/entities/plan.entity"; import * as request from "supertest"; import { Connection } from "typeorm"; import { dropStudentTable, initializeApp } from "../../test/utils"; -import { testPlan, testUser1, testUser2 } from "../../test/testingData"; +import { + testPlan, + testUser1, + testUser2, + testUser3, +} from "../../test/testingData"; describe("PlanController (e2e)", () => { let app: INestApplication; @@ -12,18 +17,17 @@ describe("PlanController (e2e)", () => { let uuid: string; let planID: number; - beforeEach(async () => { + beforeAll(async () => { app = await initializeApp(); connection = app.get(Connection); - // create student const res = await request(app.getHttpServer()) .post("/auth/register") .send(testUser1); // save accessToken and user ID - cookie = res.header["set-cookie"]; + cookie = res.headers["set-cookie"]; uuid = res.body.uuid; // insert plan into db @@ -39,18 +43,19 @@ describe("PlanController (e2e)", () => { .createQueryBuilder() .select("plan") .from(Plan, "plan") - .where("plan.name = :name", { name: "Test Plan" }) + .where("plan.name = :name AND plan.student.uuid = :uuid", { + name: "Test Plan", + uuid, + }) .getOne(); planID = plan.id; }); - afterEach(async () => { - await dropStudentTable(connection); - }); - afterAll(async () => { + await dropStudentTable(connection); await app.close(); + await connection.close(); }); it("creates a plan for a signed in user", async () => { @@ -96,7 +101,7 @@ describe("PlanController (e2e)", () => { await request(app.getHttpServer()) .patch(`/plans/${planID}`) .set("Cookie", cookie) - .send({ catalogYear: 2018 }) + .send({ name: "robert is stinky" }) .expect(200); }); @@ -117,7 +122,7 @@ describe("PlanController (e2e)", () => { await request(app.getHttpServer()) .patch(`/plans/${planID}`) .set("Cookie", badCookie) - .send({ catalogYear: 2018 }) + .send({ catalogYear: 2021 }) .expect(403); }); @@ -135,7 +140,7 @@ describe("PlanController (e2e)", () => { it("fails to delete a plan that does not belong to a user", async () => { const res = await request(app.getHttpServer()) .post("/auth/register") - .send(testUser2); + .send(testUser3); const badCookie = res.header["set-cookie"]; diff --git a/packages/api-v2/test/student/student.e2e-spec.ts b/packages/api-v2/test/student/student.e2e-spec.ts index 5bff031b4..21c31ebe5 100644 --- a/packages/api-v2/test/student/student.e2e-spec.ts +++ b/packages/api-v2/test/student/student.e2e-spec.ts @@ -11,7 +11,7 @@ describe("StudentController (e2e)", () => { let connection: Connection; let uuid: string; - beforeEach(async () => { + beforeAll(async () => { app = await initializeApp(); connection = app.get(Connection); @@ -26,12 +26,10 @@ describe("StudentController (e2e)", () => { uuid = res.body.uuid; }); - afterEach(async () => { - await dropStudentTable(connection); - }); - afterAll(async () => { + await dropStudentTable(connection); await app.close(); + await connection.close(); }); it("should successfully get a student", async () => { diff --git a/packages/api-v2/test/testingData.ts b/packages/api-v2/test/testingData.ts index 2c7578e48..84a4ce624 100644 --- a/packages/api-v2/test/testingData.ts +++ b/packages/api-v2/test/testingData.ts @@ -1,11 +1,19 @@ export const testUser1 = { email: "test-1@gmail.com", - password: "1234567890", + password: "1234567890a", + passwordConfirm: "1234567890a", }; export const testUser2 = { email: "test-2@gmail.com", - password: "1234567890", + password: "1234567890a", + passwordConfirm: "1234567890a", +}; + +export const testUser3 = { + email: "test-3@gmail.com", + password: "1234567890a", + passwordConfirm: "1234567890a", }; export const onboardedUser = { @@ -25,11 +33,14 @@ export const onboardedUser = { export const testPlan = { name: "Test Plan", schedule: { - years: [2019, 2020, 2021, 2022], - yearMap: {}, + years: [ + { + year: 2022, + }, + ], }, - major: "Computer Science", + major: "Computer Science, BSCS", coopCycle: "4 year 2 co-ops", concentration: "Artificial Intelligence", - catalogYear: 2019, + catalogYear: 2022, };