From 52d9f194456bbbd7e8d14db876b495ab07fd4515 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 09:26:24 +0530 Subject: [PATCH 01/24] add voyage team member Id to techStackItem --- .../migration.sql | 5 +++++ prisma/schema.prisma | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20240512035506_add_member_id_to_tech_stack_item_table/migration.sql diff --git a/prisma/migrations/20240512035506_add_member_id_to_tech_stack_item_table/migration.sql b/prisma/migrations/20240512035506_add_member_id_to_tech_stack_item_table/migration.sql new file mode 100644 index 00000000..0bc4db83 --- /dev/null +++ b/prisma/migrations/20240512035506_add_member_id_to_tech_stack_item_table/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "TeamTechStackItem" ADD COLUMN "voyageTeamMemberId" INTEGER; + +-- AddForeignKey +ALTER TABLE "TeamTechStackItem" ADD CONSTRAINT "TeamTechStackItem_voyageTeamMemberId_fkey" FOREIGN KEY ("voyageTeamMemberId") REFERENCES "VoyageTeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8a5c5197..827ef5a2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -224,6 +224,7 @@ model VoyageTeamMember { teamResources TeamResource[] projectFeatures ProjectFeature[] checkinForms FormResponseCheckin[] + TeamTechStackItem TeamTechStackItem[] @@unique(fields: [userId, voyageTeamId], name: "userVoyageId") } @@ -242,13 +243,15 @@ model TechStackCategory { } model TeamTechStackItem { - id Int @id @default(autoincrement()) - name String @db.Citext - category TechStackCategory? @relation(fields: [categoryId], references: [id], onUpdate: Cascade, onDelete: SetNull) - categoryId Int? - voyageTeam VoyageTeam @relation(fields: [voyageTeamId], references: [id], onUpdate: Cascade, onDelete: Cascade) - voyageTeamId Int - isSelected Boolean @default(false) + id Int @id @default(autoincrement()) + name String @db.Citext + category TechStackCategory? @relation(fields: [categoryId], references: [id], onUpdate: Cascade, onDelete: SetNull) + categoryId Int? + voyageTeam VoyageTeam @relation(fields: [voyageTeamId], references: [id], onUpdate: Cascade, onDelete: Cascade) + voyageTeamId Int + isSelected Boolean @default(false) + addedBy VoyageTeamMember? @relation(fields: [voyageTeamMemberId], references: [id], onUpdate: Cascade, onDelete: SetNull) + voyageTeamMemberId Int? createdAt DateTime @default(now()) @db.Timestamptz() updatedAt DateTime @updatedAt @@ -292,7 +295,7 @@ model ProjectIdea { title String description String vision String - isSelected Boolean @default(false) + isSelected Boolean @default(false) createdAt DateTime @default(now()) @db.Timestamptz() updatedAt DateTime @updatedAt From cbfd4c164ca4be49bd16fb4794a7618a6f188ddc Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 09:43:04 +0530 Subject: [PATCH 02/24] updated seed data to connect to voyageTeamMemberId --- prisma/seed/voyage-teams.ts | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/prisma/seed/voyage-teams.ts b/prisma/seed/voyage-teams.ts index 032334ae..8fa1fa80 100644 --- a/prisma/seed/voyage-teams.ts +++ b/prisma/seed/voyage-teams.ts @@ -255,6 +255,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[0].id, + }, + }, }, ], }, @@ -284,6 +289,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[2].id, + }, + }, }, ], }, @@ -313,6 +323,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[2].id, + }, + }, }, ], }, @@ -342,6 +357,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[1].id, + }, + }, }, ], }, @@ -371,6 +391,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[0].id, + }, + }, }, ], }, @@ -400,6 +425,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[3].id, + }, + }, }, ], }, @@ -429,6 +459,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[2].id, + }, + }, }, ], }, @@ -458,6 +493,11 @@ export const populateVoyageTeams = async () => { }, }, }, + addedBy: { + connect: { + id: voyageTeamMembers[3].id, + }, + }, }, ], }, From 9773f7be6c438f3a254db4e6b34dac9850fbfa81 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 10:38:41 +0530 Subject: [PATCH 03/24] added member id to addNewTeamTech --- src/techs/dto/create-tech.dto.ts | 5 +++++ src/techs/techs.service.ts | 8 +++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/techs/dto/create-tech.dto.ts b/src/techs/dto/create-tech.dto.ts index 64522d1a..e2bbaee4 100644 --- a/src/techs/dto/create-tech.dto.ts +++ b/src/techs/dto/create-tech.dto.ts @@ -11,4 +11,9 @@ export class CreateTeamTechDto { @IsNotEmpty() @ApiProperty({ example: 1 }) techCategoryId: number; + + @IsInt() + @IsNotEmpty() + @ApiProperty({ example: 4 }) + voyageTeamMemberId: number; } diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 4ecb152d..d41be416 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -126,16 +126,14 @@ export class TechsService { teamId: number, createTechVoteDto: CreateTeamTechDto, ) { - const voyageMemberId = await this.findVoyageMemberId(req, teamId); - if (!voyageMemberId) - throw new BadRequestException("Invalid User or Team Id"); - + // Todo: To Check if this voyageTeamMemberId is in the voyageTeam try { const newTeamTechItem = await this.prisma.teamTechStackItem.create({ data: { name: createTechVoteDto.techName, categoryId: createTechVoteDto.techCategoryId, voyageTeamId: teamId, + voyageTeamMemberId: createTechVoteDto.voyageTeamMemberId, }, }); @@ -143,7 +141,7 @@ export class TechsService { await this.prisma.teamTechStackItemVote.create({ data: { teamTechId: newTeamTechItem.id, - teamMemberId: voyageMemberId, + teamMemberId: createTechVoteDto.voyageTeamMemberId, }, }); return { From ad7674b69bbc603b87d46dd2e489ae1fc5012e3c Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 13:38:17 +0530 Subject: [PATCH 04/24] added update endpoint for tech stack item --- src/techs/dto/update-tech.dto.ts | 24 ++++++++++ src/techs/techs.controller.ts | 51 ++++++++++++++++++++++ src/techs/techs.response.ts | 17 ++++++++ src/techs/techs.service.ts | 75 +++++++++++++++++++++++++++++++- 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/techs/dto/update-tech.dto.ts diff --git a/src/techs/dto/update-tech.dto.ts b/src/techs/dto/update-tech.dto.ts new file mode 100644 index 00000000..98580278 --- /dev/null +++ b/src/techs/dto/update-tech.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt, IsNotEmpty, IsString } from "class-validator"; +import { PartialType } from "@nestjs/mapped-types"; +import { CreateTeamTechDto } from "./create-tech.dto"; + +export class UpdateTeamTechDto extends PartialType(CreateTeamTechDto) { + @IsString() + @IsNotEmpty() + @ApiProperty({ example: "Typescipt" }) + techName: string; + + @IsInt() + @IsNotEmpty() + @ApiProperty({ example: 1 }) + voyageTeamMemberId: number; + + @IsInt() + @IsNotEmpty() + @ApiProperty({ + description: "tech stack item id", + example: 1, + }) + techId: number; +} diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index 2d281260..a39b82ed 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -19,6 +19,7 @@ import { TeamTechResponse, TechItemResponse, TechItemDeleteResponse, + TechItemUpdateResponse, } from "./techs.response"; import { BadRequestErrorResponse, @@ -26,6 +27,7 @@ import { NotFoundErrorResponse, UnauthorizedErrorResponse, } from "../global/responses/errors"; +import { UpdateTeamTechDto } from "./dto/update-tech.dto"; @Controller() @ApiTags("Voyage - Techs") @@ -93,6 +95,55 @@ export class TechsController { return this.techsService.addNewTeamTech(req, teamId, createTeamTechDto); } + @ApiOperation({ + summary: "Updates a existing tech stack item in the team", + description: "Requires login", + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: "Successfully updated a tech stack item", + type: TechItemUpdateResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: "User is unauthorized to perform this action", + type: UnauthorizedErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "Bad Request - Tech Stack Item couldn't be updated", + type: BadRequestErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.CONFLICT, + description: "Tech stack item already exist for the team", + type: ConflictErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "Invalid tech stack item id", + type: NotFoundErrorResponse, + }) + @ApiParam({ + name: "teamId", + description: "voyage team Id", + type: "Integer", + required: true, + example: 1, + }) + @Patch() + updateTeamTech( + @Request() req, + @Param("teamId", ParseIntPipe) teamId: number, + @Body(ValidationPipe) updateTeamTechDto: UpdateTeamTechDto, + ) { + return this.techsService.updateExistingTeamTech( + req, + teamId, + updateTeamTechDto, + ); + } + @ApiOperation({ summary: 'Votes for an existing tech / adds the voter to the votedBy list. VotedBy: "UserId:uuid"', diff --git a/src/techs/techs.response.ts b/src/techs/techs.response.ts index dd3abdea..59a570de 100644 --- a/src/techs/techs.response.ts +++ b/src/techs/techs.response.ts @@ -69,6 +69,23 @@ export class TechItemResponse { updatedAt: Date; } +export class TechItemUpdateResponse { + @ApiProperty({ example: 1 }) + id: number; + + @ApiProperty({ example: "Typescript" }) + name: string; + + @ApiProperty({ example: 1 }) + voyageTeamMemberId: number; + + @ApiProperty({ example: 2 }) + voyageTeamId: number; + + @ApiProperty({ isArray: true }) + teamTechStackItemVotes: TeamTechStackItemVote; +} + export class TechItemDeleteResponse { @ApiProperty({ example: "The vote and tech stack item were deleted" }) message: string; diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index d41be416..da16d8cb 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -3,10 +3,12 @@ import { ConflictException, Injectable, NotFoundException, + UnauthorizedException, } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { CreateTeamTechDto } from "./dto/create-tech.dto"; import { UpdateTechSelectionsDto } from "./dto/update-tech-selections.dto"; +import { UpdateTeamTechDto } from "./dto/update-tech.dto"; const MAX_SELECTION_COUNT = 3; @@ -126,7 +128,7 @@ export class TechsService { teamId: number, createTechVoteDto: CreateTeamTechDto, ) { - // Todo: To Check if this voyageTeamMemberId is in the voyageTeam + // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam try { const newTeamTechItem = await this.prisma.teamTechStackItem.create({ data: { @@ -161,6 +163,77 @@ export class TechsService { } } + async updateExistingTeamTech( + req, + teamId: number, + updateTeamTechDto: UpdateTeamTechDto, + ) { + // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam + + const { techId, voyageTeamMemberId, techName } = updateTeamTechDto; + // check if team tech item exists + const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ + where: { + id: techId, + }, + }); + if (!teamTechItem) + throw new NotFoundException( + `[Tech Service]: Team Tech Stack Item with id:${techId} not found`, + ); + + // check if the voyageTeamMemberId in request body matches the voyageTeamMemberId that created this techStackItem + + if (voyageTeamMemberId !== teamTechItem.voyageTeamMemberId) { + throw new UnauthorizedException( + "[Tech Service]: You can only update your own Tech Stack Item.", + ); + } + try { + const updateTechStackItem = + await this.prisma.teamTechStackItem.update({ + where: { + id: techId, + voyageTeamMemberId, + }, + data: { + name: updateTeamTechDto.techName, + }, + select: { + id: true, + name: true, + voyageTeamMemberId: true, + voyageTeamId: true, + teamTechStackItemVotes: { + select: { + votedBy: { + select: { + member: { + select: { + id: true, + firstName: true, + lastName: true, + avatar: true, + }, + }, + }, + }, + }, + }, + }, + }); + + return updateTechStackItem; + } catch (e) { + if (e.code === "P2002") { + throw new ConflictException( + `[Tech Service]: ${techName} already exists in the available team tech stack.`, + ); + } + throw e; + } + } + async addExistingTechVote(req, teamId, teamTechId) { // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ From adb7df520ec34652e83117015001a7168a721afe Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 13:45:48 +0530 Subject: [PATCH 05/24] minor change --- src/techs/techs.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index a39b82ed..1a509931 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -100,7 +100,7 @@ export class TechsController { description: "Requires login", }) @ApiResponse({ - status: HttpStatus.CREATED, + status: HttpStatus.OK, description: "Successfully updated a tech stack item", type: TechItemUpdateResponse, }) From 3e2679e60d476738ebd104234d2c6565c17776c2 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 23:40:14 +0530 Subject: [PATCH 06/24] added delete endpoint for tech stack item and some minor changes --- src/techs/dto/delete-tech.dto.ts | 19 +++++++++ src/techs/techs.controller.ts | 41 +++++++++++++++++++ src/techs/techs.service.ts | 69 ++++++++++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/techs/dto/delete-tech.dto.ts diff --git a/src/techs/dto/delete-tech.dto.ts b/src/techs/dto/delete-tech.dto.ts new file mode 100644 index 00000000..5f173aae --- /dev/null +++ b/src/techs/dto/delete-tech.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt, IsNotEmpty } from "class-validator"; +import { PartialType } from "@nestjs/mapped-types"; +import { CreateTeamTechDto } from "./create-tech.dto"; + +export class DeleteTeamTechDto extends PartialType(CreateTeamTechDto) { + @IsInt() + @IsNotEmpty() + @ApiProperty({ example: 1 }) + voyageTeamMemberId: number; + + @IsInt() + @IsNotEmpty() + @ApiProperty({ + description: "tech stack item id", + example: 1, + }) + techId: number; +} diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index 1a509931..da2470ec 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -28,6 +28,7 @@ import { UnauthorizedErrorResponse, } from "../global/responses/errors"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; +import { DeleteTeamTechDto } from "./dto/delete-tech.dto"; @Controller() @ApiTags("Voyage - Techs") @@ -144,6 +145,46 @@ export class TechsController { ); } + @ApiOperation({ + summary: "Delete a tech stack item of a team", + description: "Requires login", + }) + @ApiResponse({ + status: HttpStatus.OK, + description: "Tech stack item was successfully deleted", + type: TechItemDeleteResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: "User is unauthorized to perform this action", + type: UnauthorizedErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "Bad Request - Tech stack item couldn't be deleted", + type: BadRequestErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "Invalid tech stack item id", + type: NotFoundErrorResponse, + }) + @ApiParam({ + name: "teamId", + description: "voyage team Id", + type: "Integer", + required: true, + example: 2, + }) + @Delete() + deleteTeamTech( + @Request() req, + @Param("teamId", ParseIntPipe) teamId: number, + @Body(ValidationPipe) deleteTeamTechDto: DeleteTeamTechDto, + ) { + return this.techsService.deleteTeamTech(req, teamId, deleteTeamTechDto); + } + @ApiOperation({ summary: 'Votes for an existing tech / adds the voter to the votedBy list. VotedBy: "UserId:uuid"', diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index da16d8cb..22a733b6 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -9,6 +9,7 @@ import { PrismaService } from "../prisma/prisma.service"; import { CreateTeamTechDto } from "./dto/create-tech.dto"; import { UpdateTechSelectionsDto } from "./dto/update-tech-selections.dto"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; +import { DeleteTeamTechDto } from "./dto/delete-tech.dto"; const MAX_SELECTION_COUNT = 3; @@ -170,12 +171,23 @@ export class TechsService { ) { // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam - const { techId, voyageTeamMemberId, techName } = updateTeamTechDto; + const { techId, techName } = updateTeamTechDto; // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ where: { id: techId, }, + select: { + addedBy: { + select: { + member: { + select: { + id: true, + }, + }, + }, + }, + }, }); if (!teamTechItem) throw new NotFoundException( @@ -184,7 +196,7 @@ export class TechsService { // check if the voyageTeamMemberId in request body matches the voyageTeamMemberId that created this techStackItem - if (voyageTeamMemberId !== teamTechItem.voyageTeamMemberId) { + if (req.user.userId !== teamTechItem.addedBy.member.id) { throw new UnauthorizedException( "[Tech Service]: You can only update your own Tech Stack Item.", ); @@ -194,7 +206,6 @@ export class TechsService { await this.prisma.teamTechStackItem.update({ where: { id: techId, - voyageTeamMemberId, }, data: { name: updateTeamTechDto.techName, @@ -234,6 +245,58 @@ export class TechsService { } } + async deleteTeamTech( + req, + teamId: number, + deleteTeamTechDto: DeleteTeamTechDto, + ) { + // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam + const { techId } = deleteTeamTechDto; + // check if team tech item exists + const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ + where: { + id: techId, + }, + select: { + addedBy: { + select: { + member: { + select: { + id: true, + }, + }, + }, + }, + }, + }); + + if (!teamTechItem) + throw new NotFoundException( + `[Tech Service]: Team Tech Stack Item with id:${techId} not found`, + ); + + // check if the logged in user is the one who added the tech stack item + + if (req.user.userId !== teamTechItem.addedBy.member.id) { + throw new UnauthorizedException( + "[Tech Service]: You can only delete your own Tech Stack Item.", + ); + } + try { + await this.prisma.teamTechStackItem.delete({ + where: { + id: techId, + }, + }); + return { + message: "The vote and tech stack item were deleted", + statusCode: 200, + }; + } catch (e) { + throw e; + } + } + async addExistingTechVote(req, teamId, teamTechId) { // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ From a80a621724f440d796f320e40417fbeb1452f3f4 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 12 May 2024 23:42:43 +0530 Subject: [PATCH 07/24] minor changes --- src/techs/techs.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 22a733b6..915e7896 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -194,7 +194,7 @@ export class TechsService { `[Tech Service]: Team Tech Stack Item with id:${techId} not found`, ); - // check if the voyageTeamMemberId in request body matches the voyageTeamMemberId that created this techStackItem + // check if the logged in user is the one who added the tech stack item if (req.user.userId !== teamTechItem.addedBy.member.id) { throw new UnauthorizedException( From 969eecf50045dbe18eec3564ecef7168899b614e Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 13 May 2024 12:45:04 +0530 Subject: [PATCH 08/24] added e2e tests for patch endpoint --- prisma/seed/voyage-teams.ts | 4 +- src/techs/dto/update-tech.dto.ts | 4 +- src/techs/techs.service.ts | 7 ++ test/techs.e2e-spec.ts | 156 ++++++++++++++++++++++++++++++- 4 files changed, 164 insertions(+), 7 deletions(-) diff --git a/prisma/seed/voyage-teams.ts b/prisma/seed/voyage-teams.ts index 8fa1fa80..927acfb6 100644 --- a/prisma/seed/voyage-teams.ts +++ b/prisma/seed/voyage-teams.ts @@ -228,7 +228,9 @@ export const populateVoyageTeams = async () => { }, }); - const voyageTeamMembers = await prisma.voyageTeamMember.findMany({}); + const voyageTeamMembers = ( + await prisma.voyageTeamMember.findMany({}) + ).reverse(); // nested createMany is not supported, so creating one by one // https://github.com/prisma/prisma/issues/5455 diff --git a/src/techs/dto/update-tech.dto.ts b/src/techs/dto/update-tech.dto.ts index 98580278..1e412699 100644 --- a/src/techs/dto/update-tech.dto.ts +++ b/src/techs/dto/update-tech.dto.ts @@ -1,9 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsInt, IsNotEmpty, IsString } from "class-validator"; -import { PartialType } from "@nestjs/mapped-types"; -import { CreateTeamTechDto } from "./create-tech.dto"; -export class UpdateTeamTechDto extends PartialType(CreateTeamTechDto) { +export class UpdateTeamTechDto { @IsString() @IsNotEmpty() @ApiProperty({ example: "Typescipt" }) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 915e7896..5f23be27 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -130,6 +130,9 @@ export class TechsService { createTechVoteDto: CreateTeamTechDto, ) { // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam + + //check for valid teamId + await this.validateTeamId(teamId); try { const newTeamTechItem = await this.prisma.teamTechStackItem.create({ data: { @@ -171,6 +174,8 @@ export class TechsService { ) { // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam + await this.validateTeamId(teamId); + const { techId, techName } = updateTeamTechDto; // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ @@ -251,6 +256,8 @@ export class TechsService { deleteTeamTechDto: DeleteTeamTechDto, ) { // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam + + await this.validateTeamId(teamId); const { techId } = deleteTeamTechDto; // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index cbc88cf2..72aa62e0 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -5,7 +5,9 @@ import { AppModule } from "../src/app.module"; import { PrismaService } from "../src/prisma/prisma.service"; import { seed } from "../prisma/seed/seed"; import { extractResCookieValueByKey } from "./utils"; + const newTechName = "anotherLayerJS"; +const updatedTechName = "updatedLayerJS"; //Tests require the following state and seed data to execute successfully: //Logged in as user Jessica Williamson, member of voyage team 2, as team member 8 @@ -113,6 +115,7 @@ describe("Techs Controller (e2e)", () => { describe("POST voyages/teams/:teamId/techs - add new tech item", () => { it("should return 201 if new tech item successfully added", async () => { const teamId: number = 2; + const teamMemberId: number = 8; return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) @@ -120,6 +123,7 @@ describe("Techs Controller (e2e)", () => { .send({ techName: newTechName, techCategoryId: 1, + voyageTeamMemberId: teamMemberId, }) .expect(201) .expect("Content-Type", /json/) @@ -148,6 +152,7 @@ describe("Techs Controller (e2e)", () => { it("should return 401 unauthorized if not logged in", async () => { const teamId: number = 2; + const teamMemberId: number = 8; return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) @@ -155,6 +160,7 @@ describe("Techs Controller (e2e)", () => { .send({ techName: newTechName, techCategoryId: 1, + voyageTeamMemberId: teamMemberId, }) .expect(401) .expect("Content-Type", /json/) @@ -168,8 +174,9 @@ describe("Techs Controller (e2e)", () => { }); }); - it("should return 400 if invalid teamId provided", async () => { + it("should return 404 if invalid teamId provided", async () => { const teamId: number = 9999999; + const teamMemberId: number = 8; return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) @@ -177,15 +184,16 @@ describe("Techs Controller (e2e)", () => { .send({ techName: newTechName, techCategoryId: 1, + voyageTeamMemberId: teamMemberId, }) - .expect(400) + .expect(404) .expect("Content-Type", /json/) .expect((res) => { expect(res.body).toEqual( expect.objectContaining({ message: expect.any(String), error: expect.any(String), - statusCode: 400, + statusCode: 404, }), ); }); @@ -193,6 +201,7 @@ describe("Techs Controller (e2e)", () => { it("should return 409 if tech already exists in database", async () => { const teamId: number = 2; + const teamMemberId: number = 8; return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) @@ -200,6 +209,7 @@ describe("Techs Controller (e2e)", () => { .send({ techName: newTechName, techCategoryId: 1, + voyageTeamMemberId: teamMemberId, }) .expect(409) .expect("Content-Type", /json/) @@ -215,6 +225,146 @@ describe("Techs Controller (e2e)", () => { }); }); + describe("PATCH voyages/teams/:teamId/techs - update tech stack item of the team", () => { + it("should return 200 and update a tech stack item", async () => { + const teamId: number = 2; + const techId: number = 1; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techName: updatedTechName, + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(200) + .expect("Content-Type", /json/) + .expect((res) => { + expect(res.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + voyageTeamMemberId: expect.any(Number), + voyageTeamId: expect.any(Number), + teamTechStackItemVotes: expect.any(Array), + }), + ); + expect(res.body.teamTechStackItemVotes).toEqual( + expect.arrayContaining([ + { + votedBy: { + member: { + id: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + avatar: expect.any(String), + }, + }, + }, + ]), + ); + expect(res.body.name).toEqual(updatedTechName); + }); + }); + it("should return 404 for invalid tech Id", async () => { + const teamId: number = 2; + const techId: number = 9999999; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techName: updatedTechName, + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(404) + .expect("Content-Type", /json/) + .expect((res) => { + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + error: expect.any(String), + statusCode: 404, + }), + ); + }); + }); + it("should return 400 for invalid request body", async () => { + const teamId: number = 2; + const techId: number = 1; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(400); + }); + it("should return 400 for invalid request body", async () => { + const teamId: number = 2; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techName: updatedTechName, + voyageTeamMemberId: teamMemberId, + }) + .expect(400); + }); + it("should return 400 for invalid request body", async () => { + const teamId: number = 2; + const techId: number = 1; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techName: updatedTechName, + techId: techId, + }) + .expect(400); + }); + it("should return 401 if a user tries to PATCH a tech stack item created by someone else", async () => { + const teamId: number = 2; + const techId: number = 3; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techName: updatedTechName, + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(401); + }); + it("should return 401 unauthorized if not logged in", async () => { + const teamId: number = 2; + const techId: number = 1; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .patch(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${undefined}`) + .send({ + techName: updatedTechName, + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(401); + }); + }); + describe("POST voyages/teams/:teamId/techs/:teamTechId - add user vote for tech item", () => { it("should return 200 if vote successfully added", async () => { const teamId: number = 2; From 8410b87b681efb370adbd52b93a96401b51fa2aa Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 13 May 2024 17:55:31 +0530 Subject: [PATCH 09/24] added e2e tests for delete end point and some minor changes --- src/techs/dto/delete-tech.dto.ts | 4 +- src/techs/techs.service.ts | 2 +- test/techs.e2e-spec.ts | 134 ++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/src/techs/dto/delete-tech.dto.ts b/src/techs/dto/delete-tech.dto.ts index 5f173aae..912b5aeb 100644 --- a/src/techs/dto/delete-tech.dto.ts +++ b/src/techs/dto/delete-tech.dto.ts @@ -1,9 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsInt, IsNotEmpty } from "class-validator"; -import { PartialType } from "@nestjs/mapped-types"; -import { CreateTeamTechDto } from "./create-tech.dto"; -export class DeleteTeamTechDto extends PartialType(CreateTeamTechDto) { +export class DeleteTeamTechDto { @IsInt() @IsNotEmpty() @ApiProperty({ example: 1 }) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 5f23be27..804817da 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -296,7 +296,7 @@ export class TechsService { }, }); return { - message: "The vote and tech stack item were deleted", + message: "The tech stack item is deleted", statusCode: 200, }; } catch (e) { diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index 72aa62e0..07035080 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -350,7 +350,7 @@ describe("Techs Controller (e2e)", () => { }); it("should return 401 unauthorized if not logged in", async () => { const teamId: number = 2; - const techId: number = 1; + const techId: number = 5; const teamMemberId: number = 8; return request(app.getHttpServer()) @@ -364,6 +364,138 @@ describe("Techs Controller (e2e)", () => { .expect(401); }); }); + describe("DELETE voyages/teams/:teamId/techs - delete tech stack item", () => { + it("should return 200 after deleting a tech stack item", async () => { + const teamId: number = 2; + const techId: number = 5; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(200) + .expect(async (res) => { + const deletedTechStackItem = + await prisma.teamTechStackItem.findFirst({ + where: { + id: techId, + }, + }); + expect(deletedTechStackItem).toBeNull(); + }); + }); + it("should return 404 if invalid tech id provided", async () => { + const teamId: number = 2; + const techId: number = 9999999; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(404) + .expect((res) => { + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + error: expect.any(String), + statusCode: 404, + }), + ); + }); + }); + it("should return 404 if invalid team id is provided", async () => { + const teamId: number = 9999999; + const techId: number = 1; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(404) + .expect((res) => { + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + error: expect.any(String), + statusCode: 404, + }), + ); + }); + }); + it("should return 401 if a user tries to DELETE a resource created by someone else", async () => { + const teamId: number = 2; + const techId: number = 3; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(401) + .expect((res) => { + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + error: expect.any(String), + statusCode: 401, + }), + ); + }); + }); + it("should return 400 if invalid request body", async () => { + const teamId: number = 2; + const techId: number = 5; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + techId: techId, + }) + .expect(400); + }); + it("should return 400 if invalid request body", async () => { + const teamId: number = 2; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${userAccessToken}`) + .send({ + voyageTeamMemberId: teamMemberId, + }) + .expect(400); + }); + it("should return 401 if user is not logged in", async () => { + const teamId: number = 2; + const techId: number = 5; + const teamMemberId: number = 8; + + return request(app.getHttpServer()) + .delete(`/voyages/teams/${teamId}/techs`) + .set("Authorization", `Bearer ${undefined}`) + .send({ + techId: techId, + voyageTeamMemberId: teamMemberId, + }) + .expect(401); + }); + }); describe("POST voyages/teams/:teamId/techs/:teamTechId - add user vote for tech item", () => { it("should return 200 if vote successfully added", async () => { From 969cb815d901e9208857f3339dd458da6ad96a4d Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 13 May 2024 19:28:04 +0530 Subject: [PATCH 10/24] minor changes in swagger doc --- src/techs/dto/create-tech.dto.ts | 2 +- src/techs/dto/delete-tech.dto.ts | 2 +- src/techs/dto/update-tech.dto.ts | 2 +- src/techs/techs.controller.ts | 4 ++-- src/techs/techs.response.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/techs/dto/create-tech.dto.ts b/src/techs/dto/create-tech.dto.ts index e2bbaee4..b17a76f1 100644 --- a/src/techs/dto/create-tech.dto.ts +++ b/src/techs/dto/create-tech.dto.ts @@ -14,6 +14,6 @@ export class CreateTeamTechDto { @IsInt() @IsNotEmpty() - @ApiProperty({ example: 4 }) + @ApiProperty({ example: 8 }) voyageTeamMemberId: number; } diff --git a/src/techs/dto/delete-tech.dto.ts b/src/techs/dto/delete-tech.dto.ts index 912b5aeb..81cbfee2 100644 --- a/src/techs/dto/delete-tech.dto.ts +++ b/src/techs/dto/delete-tech.dto.ts @@ -4,7 +4,7 @@ import { IsInt, IsNotEmpty } from "class-validator"; export class DeleteTeamTechDto { @IsInt() @IsNotEmpty() - @ApiProperty({ example: 1 }) + @ApiProperty({ example: 8 }) voyageTeamMemberId: number; @IsInt() diff --git a/src/techs/dto/update-tech.dto.ts b/src/techs/dto/update-tech.dto.ts index 1e412699..a760c7c3 100644 --- a/src/techs/dto/update-tech.dto.ts +++ b/src/techs/dto/update-tech.dto.ts @@ -9,7 +9,7 @@ export class UpdateTeamTechDto { @IsInt() @IsNotEmpty() - @ApiProperty({ example: 1 }) + @ApiProperty({ example: 8 }) voyageTeamMemberId: number; @IsInt() diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index da2470ec..1e846d00 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -85,7 +85,7 @@ export class TechsController { description: "voyage team Id", type: "Integer", required: true, - example: 1, + example: 2, }) @Post() addNewTeamTech( @@ -130,7 +130,7 @@ export class TechsController { description: "voyage team Id", type: "Integer", required: true, - example: 1, + example: 2, }) @Patch() updateTeamTech( diff --git a/src/techs/techs.response.ts b/src/techs/techs.response.ts index 59a570de..49dc2cb5 100644 --- a/src/techs/techs.response.ts +++ b/src/techs/techs.response.ts @@ -76,7 +76,7 @@ export class TechItemUpdateResponse { @ApiProperty({ example: "Typescript" }) name: string; - @ApiProperty({ example: 1 }) + @ApiProperty({ example: 8 }) voyageTeamMemberId: number; @ApiProperty({ example: 2 }) From 31e6c22339d1e9454a03d8422e2dac621de829d4 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Tue, 14 May 2024 09:25:14 +0530 Subject: [PATCH 11/24] updated channellog.md --- CHANGELOG.md | 1 + test/techs.e2e-spec.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e9b41f..af490e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Another example [here](https://co-pilot.dev/changelog) - Add CASL ability for Access control ([#141](https://github.com/chingu-x/chingu-dashboard-be/pull/141)) - Add sprint checkin form submission status for a user ([#149](https://github.com/chingu-x/chingu-dashboard-be/pull/149)) - new command to run both e2e and unit test ([#148](https://github.com/chingu-x/chingu-dashboard-be/pull/148)) +- allow edit and delete for tech stack item([#152](https://github.com/chingu-x/chingu-dashboard-be/pull/152)) ### Changed diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index 07035080..a3510831 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -378,7 +378,7 @@ describe("Techs Controller (e2e)", () => { voyageTeamMemberId: teamMemberId, }) .expect(200) - .expect(async (res) => { + .expect(async () => { const deletedTechStackItem = await prisma.teamTechStackItem.findFirst({ where: { From 6d3bc2799633b8caaea18b02cfd8095f89f5a434 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Wed, 15 May 2024 11:11:58 +0530 Subject: [PATCH 12/24] updated all tests of tech from bearer token to http cookie --- test/techs.e2e-spec.ts | 82 +++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index a3510831..cc4e6a38 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -4,7 +4,8 @@ import * as request from "supertest"; import { AppModule } from "../src/app.module"; import { PrismaService } from "../src/prisma/prisma.service"; import { seed } from "../prisma/seed/seed"; -import { extractResCookieValueByKey } from "./utils"; +import { loginAndGetTokens } from "./utils"; +import * as cookieParser from "cookie-parser"; const newTechName = "anotherLayerJS"; const updatedTechName = "updatedLayerJS"; @@ -17,23 +18,7 @@ const updatedTechName = "updatedLayerJS"; describe("Techs Controller (e2e)", () => { let app: INestApplication; let prisma: PrismaService; - let userAccessToken: string; - - async function loginUser() { - await request(app.getHttpServer()) - .post("/auth/login") - .send({ - email: "jessica.williamson@gmail.com", - password: "password", - }) - .expect(200) - .then((res) => { - userAccessToken = extractResCookieValueByKey( - res.headers["set-cookie"], - "access_token", - ); - }); - } + let accessToken: any; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -44,6 +29,7 @@ describe("Techs Controller (e2e)", () => { app = moduleFixture.createNestApplication(); prisma = moduleFixture.get(PrismaService); app.useGlobalPipes(new ValidationPipe()); + app.use(cookieParser()); await app.init(); }); @@ -53,7 +39,13 @@ describe("Techs Controller (e2e)", () => { }); beforeEach(async () => { - await loginUser(); + await loginAndGetTokens( + "jessica.williamson@gmail.com", + "password", + app, + ).then((tokens) => { + accessToken = tokens.access_token; + }); }); describe("GET voyages/teams/:teamId/techs - get data on all tech categories and items", () => { @@ -62,7 +54,7 @@ describe("Techs Controller (e2e)", () => { return await request(app.getHttpServer()) .get(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(200) .expect("Content-Type", /json/) .expect((res) => { @@ -119,7 +111,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: newTechName, techCategoryId: 1, @@ -180,7 +172,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: newTechName, techCategoryId: 1, @@ -205,7 +197,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: newTechName, techCategoryId: 1, @@ -233,7 +225,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: updatedTechName, techId: techId, @@ -275,7 +267,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: updatedTechName, techId: techId, @@ -300,7 +292,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, voyageTeamMemberId: teamMemberId, @@ -313,7 +305,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: updatedTechName, voyageTeamMemberId: teamMemberId, @@ -326,7 +318,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: updatedTechName, techId: techId, @@ -340,7 +332,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techName: updatedTechName, techId: techId, @@ -372,7 +364,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, voyageTeamMemberId: teamMemberId, @@ -395,7 +387,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, voyageTeamMemberId: teamMemberId, @@ -418,7 +410,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, voyageTeamMemberId: teamMemberId, @@ -441,7 +433,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, voyageTeamMemberId: teamMemberId, @@ -463,7 +455,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ techId: techId, }) @@ -475,7 +467,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ voyageTeamMemberId: teamMemberId, }) @@ -504,7 +496,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(201) .expect("Content-Type", /json/) .expect((res) => { @@ -555,7 +547,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(400) .expect("Content-Type", /json/) .expect((res) => { @@ -575,7 +567,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .post(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(409) .expect("Content-Type", /json/) .expect((res) => { @@ -597,7 +589,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(200) .expect("Content-Type", /json/) .expect((res) => { @@ -625,7 +617,7 @@ describe("Techs Controller (e2e)", () => { const techId: number = 9; return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(200) .expect("Content-Type", /json/) .expect((res) => { @@ -673,7 +665,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(400) .expect("Content-Type", /json/) .expect((res) => { @@ -693,7 +685,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .delete(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .expect(404) .expect("Content-Type", /json/) .expect((res) => { @@ -714,7 +706,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs/selections`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ categories: [ { @@ -748,7 +740,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs/selections`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ categories: [ { @@ -782,7 +774,7 @@ describe("Techs Controller (e2e)", () => { return request(app.getHttpServer()) .patch(`/voyages/teams/${teamId}/techs/selections`) - .set("Authorization", `Bearer ${userAccessToken}`) + .set("Cookie", accessToken) .send({ categories: [ { From 9a8df9ccdc8ca16fbfc11e22c2f9d2495d5fd97b Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Wed, 15 May 2024 11:35:35 +0530 Subject: [PATCH 13/24] changed unauthorized resp to forbidden resp --- src/techs/techs.controller.ts | 9 +++++---- src/techs/techs.service.ts | 6 +++--- test/techs.e2e-spec.ts | 10 +++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index 1e846d00..81be7467 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -24,6 +24,7 @@ import { import { BadRequestErrorResponse, ConflictErrorResponse, + ForbiddenErrorResponse, NotFoundErrorResponse, UnauthorizedErrorResponse, } from "../global/responses/errors"; @@ -106,9 +107,9 @@ export class TechsController { type: TechItemUpdateResponse, }) @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, + status: HttpStatus.FORBIDDEN, description: "User is unauthorized to perform this action", - type: UnauthorizedErrorResponse, + type: ForbiddenErrorResponse, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, @@ -155,9 +156,9 @@ export class TechsController { type: TechItemDeleteResponse, }) @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, + status: HttpStatus.FORBIDDEN, description: "User is unauthorized to perform this action", - type: UnauthorizedErrorResponse, + type: ForbiddenErrorResponse, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 804817da..9f1dd1e9 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, ConflictException, + ForbiddenException, Injectable, NotFoundException, - UnauthorizedException, } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { CreateTeamTechDto } from "./dto/create-tech.dto"; @@ -202,7 +202,7 @@ export class TechsService { // check if the logged in user is the one who added the tech stack item if (req.user.userId !== teamTechItem.addedBy.member.id) { - throw new UnauthorizedException( + throw new ForbiddenException( "[Tech Service]: You can only update your own Tech Stack Item.", ); } @@ -285,7 +285,7 @@ export class TechsService { // check if the logged in user is the one who added the tech stack item if (req.user.userId !== teamTechItem.addedBy.member.id) { - throw new UnauthorizedException( + throw new ForbiddenException( "[Tech Service]: You can only delete your own Tech Stack Item.", ); } diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index cc4e6a38..bec38ed4 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -325,7 +325,7 @@ describe("Techs Controller (e2e)", () => { }) .expect(400); }); - it("should return 401 if a user tries to PATCH a tech stack item created by someone else", async () => { + it("should return 403 if a user tries to PATCH a tech stack item created by someone else", async () => { const teamId: number = 2; const techId: number = 3; const teamMemberId: number = 8; @@ -338,7 +338,7 @@ describe("Techs Controller (e2e)", () => { techId: techId, voyageTeamMemberId: teamMemberId, }) - .expect(401); + .expect(403); }); it("should return 401 unauthorized if not logged in", async () => { const teamId: number = 2; @@ -426,7 +426,7 @@ describe("Techs Controller (e2e)", () => { ); }); }); - it("should return 401 if a user tries to DELETE a resource created by someone else", async () => { + it("should return 403 if a user tries to DELETE a resource created by someone else", async () => { const teamId: number = 2; const techId: number = 3; const teamMemberId: number = 8; @@ -438,13 +438,13 @@ describe("Techs Controller (e2e)", () => { techId: techId, voyageTeamMemberId: teamMemberId, }) - .expect(401) + .expect(403) .expect((res) => { expect(res.body).toEqual( expect.objectContaining({ message: expect.any(String), error: expect.any(String), - statusCode: 401, + statusCode: 403, }), ); }); From 8c5d250092da3fd87cc3c0e57734ba18643e0709 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Wed, 15 May 2024 11:53:41 +0530 Subject: [PATCH 14/24] added custom request type for req argument --- src/techs/techs.controller.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index 81be7467..dc393aad 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -30,6 +30,7 @@ import { } from "../global/responses/errors"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; import { DeleteTeamTechDto } from "./dto/delete-tech.dto"; +import { CustomRequest } from "src/global/types/CustomRequest"; @Controller() @ApiTags("Voyage - Techs") @@ -90,7 +91,7 @@ export class TechsController { }) @Post() addNewTeamTech( - @Request() req, + @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @Body(ValidationPipe) createTeamTechDto: CreateTeamTechDto, ) { @@ -111,6 +112,11 @@ export class TechsController { description: "User is unauthorized to perform this action", type: ForbiddenErrorResponse, }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: "Unauthorized when user is not logged in", + type: UnauthorizedErrorResponse, + }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "Bad Request - Tech Stack Item couldn't be updated", @@ -135,7 +141,7 @@ export class TechsController { }) @Patch() updateTeamTech( - @Request() req, + @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @Body(ValidationPipe) updateTeamTechDto: UpdateTeamTechDto, ) { @@ -160,6 +166,11 @@ export class TechsController { description: "User is unauthorized to perform this action", type: ForbiddenErrorResponse, }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: "Unauthorized when user is not logged in", + type: UnauthorizedErrorResponse, + }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "Bad Request - Tech stack item couldn't be deleted", @@ -179,7 +190,7 @@ export class TechsController { }) @Delete() deleteTeamTech( - @Request() req, + @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @Body(ValidationPipe) deleteTeamTechDto: DeleteTeamTechDto, ) { From 39c9edf29a3697e41748a96d7d0a0946f4ecee67 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sat, 18 May 2024 09:16:13 +0530 Subject: [PATCH 15/24] updated the endpoints for tech --- src/app.module.ts | 2 +- src/techs/techs.controller.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 429d9682..7b0f1d71 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,7 +30,7 @@ import { AbilitiesGuard } from "./auth/guards/abilities.guard"; path: "/", module: ResourcesModule, }, - { path: "teams/:teamId/techs", module: TechsModule }, + { path: "/", module: TechsModule }, { path: "/", module: FeaturesModule }, { path: "teams/:teamId/ideations", diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index dc393aad..eaa77fc1 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -52,7 +52,7 @@ export class TechsController { required: true, example: 1, }) - @Get() + @Get("teams/:teamId/techs") getAllTechItemsByTeamId(@Param("teamId", ParseIntPipe) teamId: number) { return this.techsService.getAllTechItemsByTeamId(teamId); } @@ -89,7 +89,7 @@ export class TechsController { required: true, example: 2, }) - @Post() + @Post("teams/:teamId/techs") addNewTeamTech( @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @@ -139,7 +139,7 @@ export class TechsController { required: true, example: 2, }) - @Patch() + @Patch("techs/:teamTechId") updateTeamTech( @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @@ -188,7 +188,7 @@ export class TechsController { required: true, example: 2, }) - @Delete() + @Delete("techs/:teamTechId") deleteTeamTech( @Request() req: CustomRequest, @Param("teamId", ParseIntPipe) teamId: number, @@ -236,7 +236,7 @@ export class TechsController { required: true, example: 6, }) - @Post("/:teamTechId") + @Post("techs/:teamTechId/vote") addExistingTechVote( @Request() req, @Param("teamId", ParseIntPipe) teamId: number, @@ -283,7 +283,7 @@ export class TechsController { required: true, example: 6, }) - @Delete("/:teamTechId") + @Delete("techs/:teamTechId/vote") removeVote( @Request() req, @Param("teamId", ParseIntPipe) teamId: number, From 0d14a522394f769ffbd4a2140132796649fc4f1e Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 19 May 2024 11:12:06 +0530 Subject: [PATCH 16/24] updated patch and delete endpoints with new url --- src/techs/dto/delete-tech.dto.ts | 17 ------------ src/techs/dto/update-tech.dto.ts | 15 +---------- src/techs/techs.controller.ts | 46 +++++++++++--------------------- src/techs/techs.service.ts | 32 +++++++--------------- 4 files changed, 26 insertions(+), 84 deletions(-) delete mode 100644 src/techs/dto/delete-tech.dto.ts diff --git a/src/techs/dto/delete-tech.dto.ts b/src/techs/dto/delete-tech.dto.ts deleted file mode 100644 index 81cbfee2..00000000 --- a/src/techs/dto/delete-tech.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty } from "class-validator"; - -export class DeleteTeamTechDto { - @IsInt() - @IsNotEmpty() - @ApiProperty({ example: 8 }) - voyageTeamMemberId: number; - - @IsInt() - @IsNotEmpty() - @ApiProperty({ - description: "tech stack item id", - example: 1, - }) - techId: number; -} diff --git a/src/techs/dto/update-tech.dto.ts b/src/techs/dto/update-tech.dto.ts index a760c7c3..5c37df2a 100644 --- a/src/techs/dto/update-tech.dto.ts +++ b/src/techs/dto/update-tech.dto.ts @@ -1,22 +1,9 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty, IsString } from "class-validator"; +import { IsNotEmpty, IsString } from "class-validator"; export class UpdateTeamTechDto { @IsString() @IsNotEmpty() @ApiProperty({ example: "Typescipt" }) techName: string; - - @IsInt() - @IsNotEmpty() - @ApiProperty({ example: 8 }) - voyageTeamMemberId: number; - - @IsInt() - @IsNotEmpty() - @ApiProperty({ - description: "tech stack item id", - example: 1, - }) - techId: number; } diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index eaa77fc1..2ba40afc 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -29,7 +29,6 @@ import { UnauthorizedErrorResponse, } from "../global/responses/errors"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; -import { DeleteTeamTechDto } from "./dto/delete-tech.dto"; import { CustomRequest } from "src/global/types/CustomRequest"; @Controller() @@ -133,22 +132,22 @@ export class TechsController { type: NotFoundErrorResponse, }) @ApiParam({ - name: "teamId", - description: "voyage team Id", + name: "teamTechItemId", + description: "team tech stack item Id", type: "Integer", required: true, - example: 2, + example: 1, }) - @Patch("techs/:teamTechId") + @Patch("techs/:teamTechItemId") updateTeamTech( @Request() req: CustomRequest, - @Param("teamId", ParseIntPipe) teamId: number, + @Param("teamTechItemId", ParseIntPipe) teamTechItemId: number, @Body(ValidationPipe) updateTeamTechDto: UpdateTeamTechDto, ) { return this.techsService.updateExistingTeamTech( req, - teamId, updateTeamTechDto, + teamTechItemId, ); } @@ -182,19 +181,18 @@ export class TechsController { type: NotFoundErrorResponse, }) @ApiParam({ - name: "teamId", - description: "voyage team Id", + name: "teamTechItemId", + description: "team tech stack item Id", type: "Integer", required: true, - example: 2, + example: 1, }) - @Delete("techs/:teamTechId") + @Delete("techs/:teamTechItemId") deleteTeamTech( @Request() req: CustomRequest, - @Param("teamId", ParseIntPipe) teamId: number, - @Body(ValidationPipe) deleteTeamTechDto: DeleteTeamTechDto, + @Param("teamTechItemId", ParseIntPipe) teamTechItemId: number, ) { - return this.techsService.deleteTeamTech(req, teamId, deleteTeamTechDto); + return this.techsService.deleteTeamTech(req, teamTechItemId); } @ApiOperation({ @@ -223,20 +221,13 @@ export class TechsController { type: UnauthorizedErrorResponse, }) @ApiParam({ - name: "teamId", - description: "voyage team Id", - type: "Integer", - required: true, - example: 2, - }) - @ApiParam({ - name: "teamTechId", + name: "teamTechItemId", description: "techId of a tech the team has select (TeamTechStackItem)", type: "Integer", required: true, example: 6, }) - @Post("techs/:teamTechId/vote") + @Post("techs/:teamTechItemId/vote") addExistingTechVote( @Request() req, @Param("teamId", ParseIntPipe) teamId: number, @@ -270,14 +261,7 @@ export class TechsController { type: UnauthorizedErrorResponse, }) @ApiParam({ - name: "teamId", - description: "voyage team Id", - type: "Integer", - required: true, - example: 2, - }) - @ApiParam({ - name: "teamTechId", + name: "teamTechItemId", description: "techId of a tech the team has select (TeamTechStackItem)", type: "Integer", required: true, diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 9f1dd1e9..d88fde63 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -9,7 +9,6 @@ import { PrismaService } from "../prisma/prisma.service"; import { CreateTeamTechDto } from "./dto/create-tech.dto"; import { UpdateTechSelectionsDto } from "./dto/update-tech-selections.dto"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; -import { DeleteTeamTechDto } from "./dto/delete-tech.dto"; const MAX_SELECTION_COUNT = 3; @@ -169,18 +168,14 @@ export class TechsService { async updateExistingTeamTech( req, - teamId: number, updateTeamTechDto: UpdateTeamTechDto, + teamTechItemId: number, ) { - // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam - - await this.validateTeamId(teamId); - - const { techId, techName } = updateTeamTechDto; + const { techName } = updateTeamTechDto; // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ where: { - id: techId, + id: teamTechItemId, }, select: { addedBy: { @@ -196,7 +191,7 @@ export class TechsService { }); if (!teamTechItem) throw new NotFoundException( - `[Tech Service]: Team Tech Stack Item with id:${techId} not found`, + `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, ); // check if the logged in user is the one who added the tech stack item @@ -210,7 +205,7 @@ export class TechsService { const updateTechStackItem = await this.prisma.teamTechStackItem.update({ where: { - id: techId, + id: teamTechItemId, }, data: { name: updateTeamTechDto.techName, @@ -250,19 +245,12 @@ export class TechsService { } } - async deleteTeamTech( - req, - teamId: number, - deleteTeamTechDto: DeleteTeamTechDto, - ) { - // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam - - await this.validateTeamId(teamId); - const { techId } = deleteTeamTechDto; + async deleteTeamTech(req, teamTechItemId: number) { // check if team tech item exists + const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ where: { - id: techId, + id: teamTechItemId, }, select: { addedBy: { @@ -279,7 +267,7 @@ export class TechsService { if (!teamTechItem) throw new NotFoundException( - `[Tech Service]: Team Tech Stack Item with id:${techId} not found`, + `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, ); // check if the logged in user is the one who added the tech stack item @@ -292,7 +280,7 @@ export class TechsService { try { await this.prisma.teamTechStackItem.delete({ where: { - id: techId, + id: teamTechItemId, }, }); return { From 2c820b035e32c5f62061b2c6c38efe4f4d6cc472 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 19 May 2024 12:26:47 +0530 Subject: [PATCH 17/24] updated vote endpoints --- src/techs/techs.controller.ts | 18 +++--- src/techs/techs.service.ts | 109 +++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/techs/techs.controller.ts b/src/techs/techs.controller.ts index 2ba40afc..db97768d 100644 --- a/src/techs/techs.controller.ts +++ b/src/techs/techs.controller.ts @@ -229,11 +229,10 @@ export class TechsController { }) @Post("techs/:teamTechItemId/vote") addExistingTechVote( - @Request() req, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("teamTechId", ParseIntPipe) teamTechId: number, + @Request() req: CustomRequest, + @Param("teamTechItemId", ParseIntPipe) teamTechItemId: number, ) { - return this.techsService.addExistingTechVote(req, teamId, teamTechId); + return this.techsService.addExistingTechVote(req, teamTechItemId); } @ApiOperation({ @@ -267,13 +266,12 @@ export class TechsController { required: true, example: 6, }) - @Delete("techs/:teamTechId/vote") + @Delete("techs/:teamTechItemId/vote") removeVote( - @Request() req, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("teamTechId", ParseIntPipe) teamTechId: number, + @Request() req: CustomRequest, + @Param("teamTechItemId", ParseIntPipe) teamTechItemId: number, ) { - return this.techsService.removeVote(req, teamId, teamTechId); + return this.techsService.removeVote(req, teamTechItemId); } @ApiOperation({ @@ -304,7 +302,7 @@ export class TechsController { required: true, example: 2, }) - @Patch("/selections") + @Patch("teams/:teamId/techs/selections") updateTechStackSelections( @Request() req, @Param("teamId", ParseIntPipe) teamId: number, diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index d88fde63..e383b9e8 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -9,6 +9,7 @@ import { PrismaService } from "../prisma/prisma.service"; import { CreateTeamTechDto } from "./dto/create-tech.dto"; import { UpdateTechSelectionsDto } from "./dto/update-tech-selections.dto"; import { UpdateTeamTechDto } from "./dto/update-tech.dto"; +import { CustomRequest } from "src/global/types/CustomRequest"; const MAX_SELECTION_COUNT = 3; @@ -28,6 +29,19 @@ export class TechsService { } }; + validTeamMember = (req: CustomRequest, teamId: number) => { + // teams of which the logged in user is a member + const teams = req.user.voyageTeams; + + // check if the teamId is in the teams array + const voyageMember = teams.filter((team) => team.teamId === teamId); + if (voyageMember.length === 0) { + throw new BadRequestException("Not a Valid user"); + } + + return voyageMember[0].memberId; + }; + findVoyageMemberId = async ( req, teamId: number, @@ -167,7 +181,7 @@ export class TechsService { } async updateExistingTeamTech( - req, + req: CustomRequest, updateTeamTechDto: UpdateTeamTechDto, teamTechItemId: number, ) { @@ -246,38 +260,41 @@ export class TechsService { } async deleteTeamTech(req, teamTechItemId: number) { - // check if team tech item exists + try { + // check if team tech item exists - const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ - where: { - id: teamTechItemId, - }, - select: { - addedBy: { + const teamTechItem = await this.prisma.teamTechStackItem.findUnique( + { + where: { + id: teamTechItemId, + }, select: { - member: { + addedBy: { select: { - id: true, + member: { + select: { + id: true, + }, + }, }, }, }, }, - }, - }); - - if (!teamTechItem) - throw new NotFoundException( - `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, ); - // check if the logged in user is the one who added the tech stack item + if (!teamTechItem) + throw new NotFoundException( + `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, + ); + + // check if the logged in user is the one who added the tech stack item + + if (req.user.userId !== teamTechItem.addedBy.member.id) { + throw new ForbiddenException( + "[Tech Service]: You can only delete your own Tech Stack Item.", + ); + } - if (req.user.userId !== teamTechItem.addedBy.member.id) { - throw new ForbiddenException( - "[Tech Service]: You can only delete your own Tech Stack Item.", - ); - } - try { await this.prisma.teamTechStackItem.delete({ where: { id: teamTechItemId, @@ -292,30 +309,36 @@ export class TechsService { } } - async addExistingTechVote(req, teamId, teamTechId) { + async addExistingTechVote(req, teamTechItemId: number) { // check if team tech item exists const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ where: { - id: teamTechId, + id: teamTechItemId, }, }); + if (!teamTechItem) throw new BadRequestException("Team Tech Item not found"); - const voyageMemberId = await this.findVoyageMemberId(req, teamId); - if (!voyageMemberId) throw new BadRequestException("Invalid User"); + + // check if the user is a member of the team + // Note: This can be removed after new authorization is implemented + const voyageMemberId = this.validTeamMember( + req, + teamTechItem.voyageTeamId, + ); try { const teamMemberTechVote = await this.prisma.teamTechStackItemVote.create({ data: { - teamTechId, + teamTechId: teamTechItemId, teamMemberId: voyageMemberId, }, }); // If successful, it returns an object containing the details of the vote return { teamTechStackItemVoteId: teamMemberTechVote.id, - teamTechId, + teamTechItemId, teamMemberId: teamMemberTechVote.teamMemberId, createdAt: teamMemberTechVote.createdAt, updatedAt: teamMemberTechVote.updatedAt, @@ -323,22 +346,36 @@ export class TechsService { } catch (e) { if (e.code === "P2002") { throw new ConflictException( - `User has already voted for techId:${teamTechId}`, + `User has already voted for techId:${teamTechItemId}`, ); } throw e; } } - async removeVote(req, teamId, teamTechId) { - const voyageMemberId = await this.findVoyageMemberId(req, teamId); - if (!voyageMemberId) throw new BadRequestException("Invalid User"); + async removeVote(req: CustomRequest, teamTechItemId: number) { + // check if team tech item exists + const teamTechItem = await this.prisma.teamTechStackItem.findUnique({ + where: { + id: teamTechItemId, + }, + }); + + if (!teamTechItem) + throw new BadRequestException("Team Tech Item not found"); + + // check if the user is a member of the team + // Note: This can be removed after new authorization is implemented + const voyageMemberId = this.validTeamMember( + req, + teamTechItem.voyageTeamId, + ); try { await this.prisma.teamTechStackItemVote.delete({ where: { userTeamStackVote: { - teamTechId, + teamTechId: teamTechItemId, teamMemberId: voyageMemberId, }, }, @@ -348,7 +385,7 @@ export class TechsService { const teamTechItem = await this.prisma.teamTechStackItem.findUnique( { where: { - id: teamTechId, + id: teamTechItemId, }, select: { teamTechStackItemVotes: true, @@ -360,7 +397,7 @@ export class TechsService { // If it's empty, delete the tech item from the database using Prisma ORM await this.prisma.teamTechStackItem.delete({ where: { - id: teamTechId, + id: teamTechItemId, }, }); From 9d56cb792c8b01dc4e4775df58180b1209973ba3 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 19 May 2024 12:44:54 +0530 Subject: [PATCH 18/24] updated e2e tests of patch/delete endpoint of tech item --- test/techs.e2e-spec.ts | 143 ++++------------------------------------- 1 file changed, 12 insertions(+), 131 deletions(-) diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index bec38ed4..1cf86616 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -217,19 +217,15 @@ describe("Techs Controller (e2e)", () => { }); }); - describe("PATCH voyages/teams/:teamId/techs - update tech stack item of the team", () => { + describe("PATCH voyages/techs/:teamTechItemId - update tech stack item of the team", () => { it("should return 200 and update a tech stack item", async () => { - const teamId: number = 2; const techId: number = 1; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) + .patch(`/voyages/techs/${techId}`) .set("Cookie", accessToken) .send({ techName: updatedTechName, - techId: techId, - voyageTeamMemberId: teamMemberId, }) .expect(200) .expect("Content-Type", /json/) @@ -261,17 +257,13 @@ describe("Techs Controller (e2e)", () => { }); }); it("should return 404 for invalid tech Id", async () => { - const teamId: number = 2; const techId: number = 9999999; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) + .patch(`/voyages/techs/${techId}`) .set("Cookie", accessToken) .send({ techName: updatedTechName, - techId: techId, - voyageTeamMemberId: teamMemberId, }) .expect(404) .expect("Content-Type", /json/) @@ -286,89 +278,43 @@ describe("Techs Controller (e2e)", () => { }); }); it("should return 400 for invalid request body", async () => { - const teamId: number = 2; - const techId: number = 1; - const teamMemberId: number = 8; - - return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) - .set("Cookie", accessToken) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) - .expect(400); - }); - it("should return 400 for invalid request body", async () => { - const teamId: number = 2; - const teamMemberId: number = 8; - - return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) - .set("Cookie", accessToken) - .send({ - techName: updatedTechName, - voyageTeamMemberId: teamMemberId, - }) - .expect(400); - }); - it("should return 400 for invalid request body", async () => { - const teamId: number = 2; const techId: number = 1; return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) + .patch(`/voyages/techs/${techId}`) .set("Cookie", accessToken) - .send({ - techName: updatedTechName, - techId: techId, - }) + .send({}) .expect(400); }); it("should return 403 if a user tries to PATCH a tech stack item created by someone else", async () => { - const teamId: number = 2; const techId: number = 3; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) + .patch(`/voyages/techs/${techId}`) .set("Cookie", accessToken) .send({ techName: updatedTechName, - techId: techId, - voyageTeamMemberId: teamMemberId, }) .expect(403); }); it("should return 401 unauthorized if not logged in", async () => { - const teamId: number = 2; const techId: number = 5; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .patch(`/voyages/teams/${teamId}/techs`) + .patch(`/voyages/techs/${techId}`) .set("Authorization", `Bearer ${undefined}`) .send({ techName: updatedTechName, - techId: techId, - voyageTeamMemberId: teamMemberId, }) .expect(401); }); }); - describe("DELETE voyages/teams/:teamId/techs - delete tech stack item", () => { + describe("DELETE voyages/techs/:teamTechItemId - delete tech stack item", () => { it("should return 200 after deleting a tech stack item", async () => { - const teamId: number = 2; const techId: number = 5; - const teamMemberId: number = 8; - return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) + .delete(`/voyages/techs/${techId}`) .set("Cookie", accessToken) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) .expect(200) .expect(async () => { const deletedTechStackItem = @@ -381,40 +327,11 @@ describe("Techs Controller (e2e)", () => { }); }); it("should return 404 if invalid tech id provided", async () => { - const teamId: number = 2; const techId: number = 9999999; - const teamMemberId: number = 8; - - return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) - .set("Cookie", accessToken) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) - .expect(404) - .expect((res) => { - expect(res.body).toEqual( - expect.objectContaining({ - message: expect.any(String), - error: expect.any(String), - statusCode: 404, - }), - ); - }); - }); - it("should return 404 if invalid team id is provided", async () => { - const teamId: number = 9999999; - const techId: number = 1; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) + .delete(`/voyages/techs/${techId}`) .set("Cookie", accessToken) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) .expect(404) .expect((res) => { expect(res.body).toEqual( @@ -427,17 +344,11 @@ describe("Techs Controller (e2e)", () => { }); }); it("should return 403 if a user tries to DELETE a resource created by someone else", async () => { - const teamId: number = 2; const techId: number = 3; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) + .delete(`/voyages/techs/${techId}`) .set("Cookie", accessToken) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) .expect(403) .expect((res) => { expect(res.body).toEqual( @@ -449,42 +360,12 @@ describe("Techs Controller (e2e)", () => { ); }); }); - it("should return 400 if invalid request body", async () => { - const teamId: number = 2; - const techId: number = 5; - - return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) - .set("Cookie", accessToken) - .send({ - techId: techId, - }) - .expect(400); - }); - it("should return 400 if invalid request body", async () => { - const teamId: number = 2; - const teamMemberId: number = 8; - - return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) - .set("Cookie", accessToken) - .send({ - voyageTeamMemberId: teamMemberId, - }) - .expect(400); - }); it("should return 401 if user is not logged in", async () => { - const teamId: number = 2; const techId: number = 5; - const teamMemberId: number = 8; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs`) + .delete(`/voyages/techs/${techId}`) .set("Authorization", `Bearer ${undefined}`) - .send({ - techId: techId, - voyageTeamMemberId: teamMemberId, - }) .expect(401); }); }); From 7e2490f602203d708625f21def4016a82de6b469 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 19 May 2024 15:12:35 +0530 Subject: [PATCH 19/24] updated vote endpoint --- src/techs/techs.service.ts | 2 +- test/techs.e2e-spec.ts | 83 +++++++++----------------------------- 2 files changed, 19 insertions(+), 66 deletions(-) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index e383b9e8..75d7eb12 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -338,7 +338,7 @@ export class TechsService { // If successful, it returns an object containing the details of the vote return { teamTechStackItemVoteId: teamMemberTechVote.id, - teamTechItemId, + teamTechId: teamTechItemId, teamMemberId: teamMemberTechVote.teamMemberId, createdAt: teamMemberTechVote.createdAt, updatedAt: teamMemberTechVote.updatedAt, diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index 1cf86616..0d7d84e7 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -370,13 +370,12 @@ describe("Techs Controller (e2e)", () => { }); }); - describe("POST voyages/teams/:teamId/techs/:teamTechId - add user vote for tech item", () => { - it("should return 200 if vote successfully added", async () => { - const teamId: number = 2; - const techId: number = 3; + describe("POST voyages/techs/:teamTechItemId/vote - add user vote for tech item", () => { + it("should return 201 if vote successfully added", async () => { + const techId: number = 6; return request(app.getHttpServer()) - .post(`/voyages/teams/${teamId}/techs/${techId}`) + .post(`/voyages/techs/${techId}/vote`) .set("Cookie", accessToken) .expect(201) .expect("Content-Type", /json/) @@ -396,7 +395,7 @@ describe("Techs Controller (e2e)", () => { it("- verify that new tech vote is present in database", async () => { const techStackVote = await prisma.teamTechStackItemVote.findMany({ where: { - teamTechId: 3, + teamTechId: 6, teamMemberId: 8, }, }); @@ -404,11 +403,10 @@ describe("Techs Controller (e2e)", () => { }); it("should return 401 unauthorized if not logged in", async () => { - const teamId: number = 2; - const techId: number = 3; + const techId: number = 6; return request(app.getHttpServer()) - .post(`/voyages/teams/${teamId}/techs/${techId}`) + .post(`/voyages/techs/${techId}/vote`) .set("Authorization", `Bearer ${undefined}`) .expect(401) .expect("Content-Type", /json/) @@ -422,32 +420,11 @@ describe("Techs Controller (e2e)", () => { }); }); - it("should return 400 if invalid teamId provided", async () => { - const teamId: number = 9999999; - const techId: number = 3; - - return request(app.getHttpServer()) - .post(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Cookie", accessToken) - .expect(400) - .expect("Content-Type", /json/) - .expect((res) => { - expect(res.body).toEqual( - expect.objectContaining({ - message: expect.any(String), - error: expect.any(String), - statusCode: 400, - }), - ); - }); - }); - it("should return 409 if user vote for tech already exists", async () => { - const teamId: number = 2; - const techId: number = 3; + const techId: number = 6; return request(app.getHttpServer()) - .post(`/voyages/teams/${teamId}/techs/${techId}`) + .post(`/voyages/techs/${techId}/vote`) .set("Cookie", accessToken) .expect(409) .expect("Content-Type", /json/) @@ -463,13 +440,12 @@ describe("Techs Controller (e2e)", () => { }); }); - describe("DELETE voyages/teams/:teamId/techs/:teamTechId - delete user vote for tech", () => { + describe("DELETE voyages/techs/:teamTechItemId/vote - delete user vote for tech", () => { it("should return 200 if tech vote deleted", async () => { - const teamId: number = 2; - const techId: number = 3; + const techId: number = 6; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs/${techId}`) + .delete(`/voyages/techs/${techId}/vote`) .set("Cookie", accessToken) .expect(200) .expect("Content-Type", /json/) @@ -486,7 +462,7 @@ describe("Techs Controller (e2e)", () => { it("- verify that new tech vote is deleted from database", async () => { const techStackVote = await prisma.teamTechStackItemVote.findMany({ where: { - teamTechId: 3, + teamTechId: 6, teamMemberId: 8, }, }); @@ -494,10 +470,9 @@ describe("Techs Controller (e2e)", () => { }); it("should return 200 if tech last vote was deleted and team tech stack item is deleted", async () => { - const teamId: number = 2; const techId: number = 9; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs/${techId}`) + .delete(`/voyages/techs/${techId}/vote`) .set("Cookie", accessToken) .expect(200) .expect("Content-Type", /json/) @@ -522,11 +497,10 @@ describe("Techs Controller (e2e)", () => { }); it("should return 401 unauthorized if not logged in", async () => { - const teamId: number = 2; - const techId: number = 3; + const techId: number = 6; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs/${techId}`) + .delete(`/voyages/techs/${techId}/vote`) .set("Authorization", `Bearer ${undefined}`) .expect(401) .expect("Content-Type", /json/) @@ -540,32 +514,11 @@ describe("Techs Controller (e2e)", () => { }); }); - it("should return 400 if invalid teamId provided", async () => { - const teamId: number = 99999; - const techId: number = 3; - - return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs/${techId}`) - .set("Cookie", accessToken) - .expect(400) - .expect("Content-Type", /json/) - .expect((res) => { - expect(res.body).toEqual( - expect.objectContaining({ - message: expect.any(String), - error: "Bad Request", - statusCode: 400, - }), - ); - }); - }); - it("should return 404 if vote to delete does not exist", async () => { - const teamId: number = 2; - const techId: number = 3; + const techId: number = 6; return request(app.getHttpServer()) - .delete(`/voyages/teams/${teamId}/techs/${techId}`) + .delete(`/voyages/techs/${techId}/vote`) .set("Cookie", accessToken) .expect(404) .expect("Content-Type", /json/) From 027eb667c6f5de44ce6b588d16bb205ad4bce325 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Sun, 19 May 2024 17:07:02 +0530 Subject: [PATCH 20/24] minor changes --- prisma/seed/voyage-teams.ts | 852 +----------------------------------- 1 file changed, 16 insertions(+), 836 deletions(-) diff --git a/prisma/seed/voyage-teams.ts b/prisma/seed/voyage-teams.ts index d7cb4ca3..c9f971a7 100644 --- a/prisma/seed/voyage-teams.ts +++ b/prisma/seed/voyage-teams.ts @@ -1050,826 +1050,6 @@ export const populateVoyageTeams = async () => { }, }); - // v48 - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "48", - }, - }, - name: "v48-tier1-team-1", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/v46-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 1" }, - }, - endDate: new Date("2024-04-15T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[4].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 60, - }, - ], - }, - }, - }); - - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "48", - }, - }, - name: "v48-tier3-team-30", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/soloproject-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 3" }, - }, - endDate: new Date("2024-04-15T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[3].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[4].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 15, - }, - ], - }, - }, - }); - - // v49 - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "49", - }, - }, - name: "v49-tier1-team-1", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/v46-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 1" }, - }, - endDate: new Date("2024-06-17T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[4].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 60, - }, - ], - }, - }, - }); - - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "49", - }, - }, - name: "v49-tier3-team-30", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/soloproject-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 3" }, - }, - endDate: new Date("2024-06-17T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[3].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[4].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 15, - }, - ], - }, - }, - }); - - // v50 - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "50", - }, - }, - name: "v50-tier1-team-1", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/v46-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 1" }, - }, - endDate: new Date("2024-08-12T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[4].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 60, - }, - ], - }, - }, - }); - - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "50", - }, - }, - name: "v50-tier3-team-30", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/soloproject-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 3" }, - }, - endDate: new Date("2024-08-12T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[3].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[4].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 15, - }, - ], - }, - }, - }); - - // v51 - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "51", - }, - }, - name: "v51-tier1-team-1", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/v46-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 1" }, - }, - endDate: new Date("2024-10-14T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[4].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 60, - }, - ], - }, - }, - }); - - await prisma.voyageTeam.create({ - data: { - voyage: { - connect: { - number: "51", - }, - }, - name: "v51-tier3-team-30", - status: { - connect: { - name: "Active", - }, - }, - repoUrl: - "https://github.com/chingu-voyages/soloproject-tier3-chinguweather", - repoUrlBE: "https://github.com/chingu-voyages/Handbook", - deployedUrl: "https://www.chingu.io/", - deployedUrlBE: - "https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying", - tier: { - connect: { name: "Tier 3" }, - }, - endDate: new Date("2024-10-14T04:59:59.000Z"), - voyageTeamMembers: { - create: [ - { - member: { - connect: { - email: users[0].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[0].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 10, - }, - { - member: { - connect: { - email: users[1].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 12, - }, - { - member: { - connect: { - email: users[2].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[2].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 20, - }, - { - member: { - connect: { - email: users[3].email, - }, - }, - voyageRole: { - connect: { - name: voyageRoles[4].name, - }, - }, - status: { - connect: { - name: "Active", - }, - }, - hrPerSprint: 15, - }, - ], - }, - }, - }); - const voyageTeamMembers = await prisma.voyageTeamMember.findMany({}); /* ============== Add tech stack items, etc to teams ================== */ @@ -1893,14 +1073,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[0].id, + id: voyageTeamMembers[7].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[0].id, + id: voyageTeamMembers[7].id, }, }, }, @@ -1927,14 +1107,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, @@ -1961,14 +1141,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, @@ -1995,14 +1175,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[1].id, + id: voyageTeamMembers[5].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[1].id, + id: voyageTeamMembers[5].id, }, }, }, @@ -2029,14 +1209,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[0].id, + id: voyageTeamMembers[7].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[0].id, + id: voyageTeamMembers[7].id, }, }, }, @@ -2063,14 +1243,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[3].id, + id: voyageTeamMembers[4].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[3].id, + id: voyageTeamMembers[4].id, }, }, }, @@ -2097,14 +1277,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[2].id, + id: voyageTeamMembers[6].id, }, }, }, @@ -2131,14 +1311,14 @@ export const populateVoyageTeams = async () => { create: { votedBy: { connect: { - id: voyageTeamMembers[3].id, + id: voyageTeamMembers[7].id, }, }, }, }, addedBy: { connect: { - id: voyageTeamMembers[3].id, + id: voyageTeamMembers[7].id, }, }, }, From c12a0692837e81a9529a2e306867e61a74ae5c28 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 20 May 2024 09:33:46 +0530 Subject: [PATCH 21/24] added check for valid team id and team member id and related tests --- src/techs/techs.service.ts | 11 +++++++++-- test/techs.e2e-spec.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index 75d7eb12..aed838dc 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -142,10 +142,17 @@ export class TechsService { teamId: number, createTechVoteDto: CreateTeamTechDto, ) { - // TODO: To Check if the voyageTeamMemberId in request body is in the voyageTeam - //check for valid teamId await this.validateTeamId(teamId); + + // To Check if the voyageTeamMemberId in request body is in the voyageTeam + const voyageMemberId = await this.findVoyageMemberId(req, teamId); + if ( + !voyageMemberId || + voyageMemberId !== createTechVoteDto.voyageTeamMemberId + ) + throw new BadRequestException("Invalid User or Team Id"); + try { const newTeamTechItem = await this.prisma.teamTechStackItem.create({ data: { diff --git a/test/techs.e2e-spec.ts b/test/techs.e2e-spec.ts index 0d7d84e7..7915f5f3 100644 --- a/test/techs.e2e-spec.ts +++ b/test/techs.e2e-spec.ts @@ -142,6 +142,21 @@ describe("Techs Controller (e2e)", () => { return expect(techStackItem[0].name).toEqual(newTechName); }); + it("should return 400 if invalid team member id provided", async () => { + const teamId: number = 2; + const teamMemberId: number = 4; + + return request(app.getHttpServer()) + .post(`/voyages/teams/${teamId}/techs`) + .set("Cookie", accessToken) + .send({ + techName: newTechName, + techCategoryId: 1, + voyageTeamMemberId: teamMemberId, + }) + .expect(400); + }); + it("should return 401 unauthorized if not logged in", async () => { const teamId: number = 2; const teamMemberId: number = 8; From 4fa309c811cf54e8eec5df891578be0ccb4022fd Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 20 May 2024 10:41:48 +0530 Subject: [PATCH 22/24] restrict edit/delete if it has votes other than the owner --- src/techs/techs.service.ts | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index aed838dc..efddcaed 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -208,6 +208,19 @@ export class TechsService { }, }, }, + teamTechStackItemVotes: { + select: { + votedBy: { + select: { + member: { + select: { + id: true, + }, + }, + }, + }, + }, + }, }, }); if (!teamTechItem) @@ -222,6 +235,23 @@ export class TechsService { "[Tech Service]: You can only update your own Tech Stack Item.", ); } + // check if the tech stack item has votes other than the user created it + if (teamTechItem.teamTechStackItemVotes.length > 1) { + throw new ConflictException( + `Tech Stack Item cannot be updated when others have voted for it.`, + ); + } else { + // Here we check if it has 1 vote, and if it's the not of the same user who added the tech stack item + if ( + teamTechItem.teamTechStackItemVotes[0].votedBy.member.id !== + teamTechItem.addedBy.member.id + ) { + throw new ConflictException( + `Tech Stack Item cannot be updated when others have voted for it.`, + ); + } + } + try { const updateTechStackItem = await this.prisma.teamTechStackItem.update({ @@ -285,6 +315,19 @@ export class TechsService { }, }, }, + teamTechStackItemVotes: { + select: { + votedBy: { + select: { + member: { + select: { + id: true, + }, + }, + }, + }, + }, + }, }, }, ); @@ -301,6 +344,22 @@ export class TechsService { "[Tech Service]: You can only delete your own Tech Stack Item.", ); } + // check if the tech stack item has votes other than the user created it + if (teamTechItem.teamTechStackItemVotes.length > 1) { + throw new ConflictException( + `Tech Stack Item cannot be delete when others have voted for it.`, + ); + } else { + // Here we check if it has 1 vote, and if it's the not of the same user who added the tech stack item + if ( + teamTechItem.teamTechStackItemVotes[0].votedBy.member.id !== + teamTechItem.addedBy.member.id + ) { + throw new ConflictException( + `Tech Stack Item cannot be delete when others have voted for it.`, + ); + } + } await this.prisma.teamTechStackItem.delete({ where: { From bf5dddb16c544808edb5e15dbe6fd84043e6a284 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Fri, 24 May 2024 18:13:43 +0530 Subject: [PATCH 23/24] added ability for last voter to edit/delete the tech item --- src/techs/techs.service.ts | 58 ++++++++++++++------------------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index efddcaed..efdc12f9 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -228,28 +228,20 @@ export class TechsService { `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, ); - // check if the logged in user is the one who added the tech stack item - - if (req.user.userId !== teamTechItem.addedBy.member.id) { - throw new ForbiddenException( - "[Tech Service]: You can only update your own Tech Stack Item.", - ); - } // check if the tech stack item has votes other than the user created it if (teamTechItem.teamTechStackItemVotes.length > 1) { throw new ConflictException( - `Tech Stack Item cannot be updated when others have voted for it.`, + `[Tech Service]: Tech Stack Item cannot be updated when others have voted for it.`, + ); + } + // The person having the last vote has the ability to edit and delete the tech stack item + if ( + req.user.userId !== + teamTechItem.teamTechStackItemVotes[0].votedBy.member.id + ) { + throw new ForbiddenException( + "[Tech Service]: You cannot update this Tech Stack Item.", ); - } else { - // Here we check if it has 1 vote, and if it's the not of the same user who added the tech stack item - if ( - teamTechItem.teamTechStackItemVotes[0].votedBy.member.id !== - teamTechItem.addedBy.member.id - ) { - throw new ConflictException( - `Tech Stack Item cannot be updated when others have voted for it.`, - ); - } } try { @@ -296,7 +288,7 @@ export class TechsService { } } - async deleteTeamTech(req, teamTechItemId: number) { + async deleteTeamTech(req: CustomRequest, teamTechItemId: number) { try { // check if team tech item exists @@ -337,28 +329,20 @@ export class TechsService { `[Tech Service]: Team Tech Stack Item with id:${teamTechItemId} not found`, ); - // check if the logged in user is the one who added the tech stack item - - if (req.user.userId !== teamTechItem.addedBy.member.id) { - throw new ForbiddenException( - "[Tech Service]: You can only delete your own Tech Stack Item.", - ); - } // check if the tech stack item has votes other than the user created it if (teamTechItem.teamTechStackItemVotes.length > 1) { throw new ConflictException( - `Tech Stack Item cannot be delete when others have voted for it.`, + `[Tech Service]: Tech Stack Item cannot be delete when others have voted for it.`, + ); + } + // The person having the last vote has the ability to edit and delete the tech stack item + if ( + req.user.userId !== + teamTechItem.teamTechStackItemVotes[0].votedBy.member.id + ) { + throw new ForbiddenException( + "[Tech Service]: You cannot delete this Tech Stack Item.", ); - } else { - // Here we check if it has 1 vote, and if it's the not of the same user who added the tech stack item - if ( - teamTechItem.teamTechStackItemVotes[0].votedBy.member.id !== - teamTechItem.addedBy.member.id - ) { - throw new ConflictException( - `Tech Stack Item cannot be delete when others have voted for it.`, - ); - } } await this.prisma.teamTechStackItem.delete({ From a60dd710ffbe61dde2645b68ff4c3f47f5e56e61 Mon Sep 17 00:00:00 2001 From: Arman Kumar Jena Date: Mon, 27 May 2024 10:06:04 +0530 Subject: [PATCH 24/24] added more descriptive error messages --- src/techs/techs.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/techs/techs.service.ts b/src/techs/techs.service.ts index efdc12f9..9327295d 100644 --- a/src/techs/techs.service.ts +++ b/src/techs/techs.service.ts @@ -36,7 +36,9 @@ export class TechsService { // check if the teamId is in the teams array const voyageMember = teams.filter((team) => team.teamId === teamId); if (voyageMember.length === 0) { - throw new BadRequestException("Not a Valid user"); + throw new BadRequestException( + "User is not in the specified team, check voyageTeamId or voyageTeamMemberId is correct.", + ); } return voyageMember[0].memberId;