diff --git a/nestjs-BE/server/src/profile-space/dto/create-profile-space.dto.ts b/nestjs-BE/server/src/profile-space/dto/create-profile-space.dto.ts deleted file mode 100644 index fdd16f5c..00000000 --- a/nestjs-BE/server/src/profile-space/dto/create-profile-space.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateProfileSpaceDto { - @ApiProperty({ - example: 'space uuid', - description: 'Space UUID', - }) - @IsNotEmpty() - @IsString() - space_uuid: string; - - profile_uuid: string; -} diff --git a/nestjs-BE/server/src/profile-space/dto/update-profile-space.dto.ts b/nestjs-BE/server/src/profile-space/dto/update-profile-space.dto.ts deleted file mode 100644 index 9bef8027..00000000 --- a/nestjs-BE/server/src/profile-space/dto/update-profile-space.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateProfileSpaceDto } from './create-profile-space.dto'; - -export class UpdateProfileSpaceDto extends PartialType(CreateProfileSpaceDto) { - uuid?: string; -} diff --git a/nestjs-BE/server/src/profile-space/profile-space.controller.ts b/nestjs-BE/server/src/profile-space/profile-space.controller.ts deleted file mode 100644 index 30573c4d..00000000 --- a/nestjs-BE/server/src/profile-space/profile-space.controller.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Delete, - Param, - Request as Req, - NotFoundException, - HttpException, - HttpStatus, - ConflictException, -} from '@nestjs/common'; -import { ProfileSpaceService } from './profile-space.service'; -import { CreateProfileSpaceDto } from './dto/create-profile-space.dto'; -import { RequestWithUser } from '../utils/interface'; -import { SpacesService } from '../spaces/spaces.service'; -import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; -import { ProfilesService } from '../profiles/profiles.service'; - -@Controller('profileSpace') -@ApiTags('profileSpace') -export class ProfileSpaceController { - constructor( - private readonly profileSpaceService: ProfileSpaceService, - private readonly spacesService: SpacesService, - private readonly profilesService: ProfilesService, - ) {} - - @Post('join') - @ApiOperation({ summary: 'Join space' }) - @ApiResponse({ - status: 201, - description: 'Join data has been successfully created.', - }) - @ApiResponse({ - status: 409, - description: 'Conflict. You have already joined the space.', - }) - async create( - @Body() createProfileSpaceDto: CreateProfileSpaceDto, - @Req() req: RequestWithUser, - ) { - const profile = await this.profilesService.findProfile(req.user.uuid); - if (!profile) throw new NotFoundException(); - const profileSpace = await this.profileSpaceService.joinSpace( - profile.uuid, - createProfileSpaceDto.space_uuid, - ); - if (!profileSpace) { - throw new HttpException('Data already exists.', HttpStatus.CONFLICT); - } - return { statusCode: 201, message: 'Created', data: profileSpace }; - } - - @Delete('leave/:space_uuid') - @ApiResponse({ - status: 204, - description: 'Successfully left the space.', - }) - @ApiResponse({ - status: 404, - description: 'Space not found.', - }) - async delete( - @Param('space_uuid') spaceUuid: string, - @Req() req: RequestWithUser, - ) { - const profile = await this.profilesService.findProfile(req.user.uuid); - if (!profile) throw new NotFoundException(); - const space = await this.spacesService.findSpace(spaceUuid); - if (!space) throw new NotFoundException(); - const profileSpace = await this.profileSpaceService.leaveSpace( - profile.uuid, - spaceUuid, - ); - if (!profileSpace) throw new ConflictException(); - const isSpaceEmpty = await this.profileSpaceService.isSpaceEmpty(spaceUuid); - if (isSpaceEmpty) { - await this.spacesService.deleteSpace(spaceUuid); - } - return { statusCode: 204, message: 'No Content' }; - } - - @Get('spaces') - @ApiOperation({ summary: "Get user's spaces" }) - @ApiResponse({ - status: 200, - description: 'Returns a list of spaces.', - }) - async getSpaces(@Req() req: RequestWithUser) { - const profile = await this.profilesService.findProfile(req.user.uuid); - if (!profile) throw new NotFoundException(); - const profileSpaces = - await this.profileSpaceService.findProfileSpacesByProfileUuid( - profile.uuid, - ); - const spaceUuids = profileSpaces.map( - (profileSpace) => profileSpace.spaceUuid, - ); - const spaces = await this.spacesService.findSpaces(spaceUuids); - return { statusCode: 200, message: 'Success', data: spaces }; - } - - @Get('users/:space_uuid') - @ApiOperation({ summary: 'Get users in the space' }) - @ApiResponse({ - status: 200, - description: 'Returns a list of users.', - }) - @ApiResponse({ - status: 404, - description: 'Space not found.', - }) - async getProfiles(@Param('space_uuid') spaceUuid: string) { - const space = await this.spacesService.findSpace(spaceUuid); - if (!space) throw new NotFoundException(); - const profileSpaces = - await this.profileSpaceService.findProfileSpacesBySpaceUuid(space.uuid); - const profileUuids = profileSpaces.map( - (profileSpace) => profileSpace.profileUuid, - ); - const profiles = await this.profilesService.findProfiles(profileUuids); - return { statusCode: 200, message: 'Success', data: profiles }; - } -} diff --git a/nestjs-BE/server/src/profile-space/profile-space.module.ts b/nestjs-BE/server/src/profile-space/profile-space.module.ts index 9e662309..90e8a178 100644 --- a/nestjs-BE/server/src/profile-space/profile-space.module.ts +++ b/nestjs-BE/server/src/profile-space/profile-space.module.ts @@ -1,12 +1,9 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ProfileSpaceService } from './profile-space.service'; -import { ProfileSpaceController } from './profile-space.controller'; import { ProfilesModule } from '../profiles/profiles.module'; -import { SpacesModule } from '../spaces/spaces.module'; @Module({ - imports: [ProfilesModule, forwardRef(() => SpacesModule)], - controllers: [ProfileSpaceController], + imports: [ProfilesModule], providers: [ProfileSpaceService], exports: [ProfileSpaceService], }) diff --git a/nestjs-BE/server/src/profile-space/profile-space.service.spec.ts b/nestjs-BE/server/src/profile-space/profile-space.service.spec.ts new file mode 100644 index 00000000..40aa3d28 --- /dev/null +++ b/nestjs-BE/server/src/profile-space/profile-space.service.spec.ts @@ -0,0 +1,83 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProfileSpaceService } from './profile-space.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { ProfileSpace } from '@prisma/client'; + +describe('ProfileSpaceService', () => { + let profileSpaceService: ProfileSpaceService; + let prisma: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ProfileSpaceService, + { + provide: PrismaService, + useValue: { + profileSpace: { + findFirst: jest.fn(), + findUnique: jest.fn(), + }, + }, + }, + ], + }).compile(); + + profileSpaceService = module.get(ProfileSpaceService); + prisma = module.get(PrismaService); + }); + + it('isSpaceEmpty empty', async () => { + const spaceUuid = 'space uuid'; + + (prisma.profileSpace.findFirst as jest.Mock).mockResolvedValue(null); + + const isSpaceEmpty = profileSpaceService.isSpaceEmpty(spaceUuid); + + await expect(isSpaceEmpty).resolves.toBeTruthy(); + }); + + it('isSpaceEmpty not empty', async () => { + const spaceUuid = 'space uuid'; + const profileSpace = 'profile space'; + + (prisma.profileSpace.findFirst as jest.Mock).mockResolvedValue( + profileSpace, + ); + + const isSpaceEmpty = profileSpaceService.isSpaceEmpty(spaceUuid); + + await expect(isSpaceEmpty).resolves.toBeFalsy(); + }); + + it('isProfileInSpace joined', async () => { + const spaceUuid = 'space uuid'; + const profileUuid = 'profile uuid'; + const profileSpaceMock = { profileUuid, spaceUuid } as ProfileSpace; + + (prisma.profileSpace.findUnique as jest.Mock).mockResolvedValue( + profileSpaceMock, + ); + + const isProfileInSpace = profileSpaceService.isProfileInSpace( + profileUuid, + spaceUuid, + ); + + await expect(isProfileInSpace).resolves.toBeTruthy(); + }); + + it('isProfileInSpace not joined', async () => { + const spaceUuid = 'space uuid'; + const profileUuid = 'profile uuid'; + + (prisma.profileSpace.findUnique as jest.Mock).mockResolvedValue(null); + + const isProfileInSpace = profileSpaceService.isProfileInSpace( + profileUuid, + spaceUuid, + ); + + await expect(isProfileInSpace).resolves.toBeFalsy(); + }); +}); diff --git a/nestjs-BE/server/src/profile-space/profile-space.service.ts b/nestjs-BE/server/src/profile-space/profile-space.service.ts index 55c6c1da..851c6f79 100644 --- a/nestjs-BE/server/src/profile-space/profile-space.service.ts +++ b/nestjs-BE/server/src/profile-space/profile-space.service.ts @@ -6,19 +6,21 @@ import { Prisma, ProfileSpace } from '@prisma/client'; export class ProfileSpaceService { constructor(private readonly prisma: PrismaService) {} - async findProfileSpacesByProfileUuid( + async createProfileSpace( profileUuid: string, - ): Promise { - return this.prisma.profileSpace.findMany({ - where: { profileUuid: profileUuid }, + spaceUuid: string, + ): Promise { + return this.prisma.profileSpace.create({ + data: { spaceUuid, profileUuid }, }); } - async findProfileSpacesBySpaceUuid( + async deleteProfileSpace( + profileUuid: string, spaceUuid: string, - ): Promise { - return this.prisma.profileSpace.findMany({ - where: { spaceUuid: spaceUuid }, + ): Promise { + return this.prisma.profileSpace.delete({ + where: { spaceUuid_profileUuid: { spaceUuid, profileUuid } }, }); } @@ -48,28 +50,6 @@ export class ProfileSpaceService { } } - async leaveSpace( - profileUuid: string, - spaceUuid: string, - ): Promise { - try { - return await this.prisma.profileSpace.delete({ - where: { - spaceUuid_profileUuid: { - spaceUuid: spaceUuid, - profileUuid: profileUuid, - }, - }, - }); - } catch (err) { - if (err instanceof Prisma.PrismaClientKnownRequestError) { - return null; - } else { - throw err; - } - } - } - async isSpaceEmpty(spaceUuid: string) { const first = await this.prisma.profileSpace.findFirst({ where: { @@ -78,4 +58,14 @@ export class ProfileSpaceService { }); return first ? false : true; } + + async isProfileInSpace( + profileUuid: string, + spaceUuid: string, + ): Promise { + const profileSpace = await this.prisma.profileSpace.findUnique({ + where: { spaceUuid_profileUuid: { spaceUuid, profileUuid } }, + }); + return profileSpace ? true : false; + } } diff --git a/nestjs-BE/server/src/profiles/dto/profile-space.dto.ts b/nestjs-BE/server/src/profiles/dto/profile-space.dto.ts deleted file mode 100644 index 19fad8d4..00000000 --- a/nestjs-BE/server/src/profiles/dto/profile-space.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ProfileSpaceDto { - @ApiProperty({ - example: 'profile-uuid-123', - description: 'UUID of the profile', - }) - profile_uuid: string; - - @ApiProperty({ example: 'space-uuid-456', description: 'UUID of the space' }) - space_uuid: string; -} diff --git a/nestjs-BE/server/src/spaces/dto/create-space.dto.ts b/nestjs-BE/server/src/spaces/dto/create-space.dto.ts index 4e3414a0..11ba7b08 100644 --- a/nestjs-BE/server/src/spaces/dto/create-space.dto.ts +++ b/nestjs-BE/server/src/spaces/dto/create-space.dto.ts @@ -4,7 +4,7 @@ import { MAX_NAME_LENGTH } from '../../config/magic-number'; import { Expose } from 'class-transformer'; import { v4 as uuid } from 'uuid'; -export class CreateSpaceRequestV2Dto { +export class CreateSpaceRequestDto { @IsString() @IsNotEmpty() @MaxLength(MAX_NAME_LENGTH) @@ -24,20 +24,6 @@ export class CreateSpaceRequestV2Dto { icon: string; } -export class CreateSpaceRequestDto { - @IsString() - @IsNotEmpty() - @MaxLength(MAX_NAME_LENGTH) - @ApiProperty({ example: 'Sample Space', description: 'Name of the space' }) - name: string; - - @ApiProperty({ - example: 'space-icon.png', - description: 'Profile icon for the space', - }) - icon: string; -} - export class CreateSpacePrismaDto { name: string; icon: string; diff --git a/nestjs-BE/server/src/spaces/dto/join-space.dto.ts b/nestjs-BE/server/src/spaces/dto/join-space.dto.ts new file mode 100644 index 00000000..462a053e --- /dev/null +++ b/nestjs-BE/server/src/spaces/dto/join-space.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { v4 as uuid } from 'uuid'; + +export class JoinSpaceRequestDto { + @IsString() + @IsNotEmpty() + @Expose({ name: 'profile_uuid' }) + @ApiProperty({ example: uuid(), description: 'Profile uuid' }) + profileUuid: string; +} diff --git a/nestjs-BE/server/src/spaces/dto/update-space.dto.ts b/nestjs-BE/server/src/spaces/dto/update-space.dto.ts index 2af0b07e..56d141c5 100644 --- a/nestjs-BE/server/src/spaces/dto/update-space.dto.ts +++ b/nestjs-BE/server/src/spaces/dto/update-space.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; import { MAX_NAME_LENGTH } from '../../config/magic-number'; -export class UpdateSpaceRequestV2Dto { +export class UpdateSpaceRequestDto { @IsOptional() @IsString() @IsNotEmpty() @@ -23,25 +23,6 @@ export class UpdateSpaceRequestV2Dto { icon: string; } -export class UpdateSpaceRequestDto { - @IsString() - @IsNotEmpty() - @MaxLength(MAX_NAME_LENGTH) - @ApiProperty({ - example: 'new space', - description: 'Updated space name', - required: false, - }) - name: string; - - @ApiProperty({ - example: 'new image', - description: 'Updated space icon', - required: false, - }) - icon: string; -} - export class UpdateSpacePrismaDto { name: string; icon: string; diff --git a/nestjs-BE/server/src/spaces/spaces.controller.spec.ts b/nestjs-BE/server/src/spaces/spaces.controller.spec.ts index fdc36092..0c096ad9 100644 --- a/nestjs-BE/server/src/spaces/spaces.controller.spec.ts +++ b/nestjs-BE/server/src/spaces/spaces.controller.spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { SpacesControllerV2 } from './spaces.controller'; +import { SpacesController } from './spaces.controller'; import { SpacesService } from './spaces.service'; import { ProfileSpaceService } from '../profile-space/profile-space.service'; import { UploadService } from '../upload/upload.service'; -import { ProfilesService } from '../profiles/profiles.service'; import { Profile, Space } from '@prisma/client'; import { BadRequestException, @@ -11,23 +10,24 @@ import { HttpStatus, NotFoundException, } from '@nestjs/common'; -import { UpdateSpaceRequestV2Dto } from './dto/update-space.dto'; -import { CreateSpaceRequestV2Dto } from './dto/create-space.dto'; +import { UpdateSpaceRequestDto } from './dto/update-space.dto'; +import { CreateSpaceRequestDto } from './dto/create-space.dto'; import { RequestWithUser } from '../utils/interface'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { UsersService } from '../users/users.service'; -describe('SpacesControllerV2', () => { - let controller: SpacesControllerV2; +describe('SpacesController', () => { + let controller: SpacesController; let spacesService: SpacesService; let uploadService: UploadService; - let profilesService: ProfilesService; let configService: ConfigService; let profileSpaceService: ProfileSpaceService; + let usersService: UsersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule], - controllers: [SpacesControllerV2], + controllers: [SpacesController], providers: [ { provide: SpacesService, @@ -35,6 +35,9 @@ describe('SpacesControllerV2', () => { createSpace: jest.fn(), findSpace: jest.fn(), updateSpace: jest.fn(), + joinSpace: jest.fn(), + leaveSpace: jest.fn(), + findProfilesInSpace: jest.fn(), }, }, { provide: UploadService, useValue: { uploadFile: jest.fn() } }, @@ -45,22 +48,16 @@ describe('SpacesControllerV2', () => { joinSpace: jest.fn(), }, }, - { - provide: ProfilesService, - useValue: { - findProfile: jest.fn(), - findProfileByProfileUuid: jest.fn(), - }, - }, + { provide: UsersService, useValue: { verifyUserProfile: jest.fn() } }, ], }).compile(); - controller = module.get(SpacesControllerV2); + controller = module.get(SpacesController); spacesService = module.get(SpacesService); uploadService = module.get(UploadService); - profilesService = module.get(ProfilesService); configService = module.get(ConfigService); profileSpaceService = module.get(ProfileSpaceService); + usersService = module.get(UsersService); }); it('create created', async () => { @@ -74,14 +71,12 @@ describe('SpacesControllerV2', () => { const bodyMock = { name: 'new space name', profileUuid: profileMock.uuid, - } as CreateSpaceRequestV2Dto; + } as CreateSpaceRequestDto; const spaceMock = { uuid: 'space uuid' } as Space; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue(iconUrlMock); - jest.spyOn(spacesService, 'createSpace').mockResolvedValue(spaceMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + (uploadService.uploadFile as jest.Mock).mockResolvedValue(iconUrlMock); + (spacesService.createSpace as jest.Mock).mockResolvedValue(spaceMock); const response = controller.create(iconMock, bodyMock, requestMock); @@ -103,12 +98,12 @@ describe('SpacesControllerV2', () => { const bodyMock = { name: 'new space name', profileUuid: 'wrong profile uuid', - } as CreateSpaceRequestV2Dto; + } as CreateSpaceRequestDto; const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); const response = controller.create(iconMock, bodyMock, requestMock); @@ -127,11 +122,11 @@ describe('SpacesControllerV2', () => { const bodyMock = { name: 'new space name', profileUuid: profileMock.uuid, - } as CreateSpaceRequestV2Dto; + } as CreateSpaceRequestDto; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); const response = controller.create(iconMock, bodyMock, requestMock); @@ -149,13 +144,11 @@ describe('SpacesControllerV2', () => { const bodyMock = { name: 'new space name', profileUuid: profileMock.uuid, - } as CreateSpaceRequestV2Dto; + } as CreateSpaceRequestDto; const spaceMock = { uuid: 'space uuid' } as Space; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest.spyOn(spacesService, 'createSpace').mockResolvedValue(spaceMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + (spacesService.createSpace as jest.Mock).mockResolvedValue(spaceMock); const response = controller.create( null as unknown as Express.Multer.File, @@ -188,13 +181,11 @@ describe('SpacesControllerV2', () => { profileUuid: profileMock.uuid, }; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest.spyOn(spacesService, 'findSpace').mockResolvedValue(spaceMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(profileSpaceMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + (spacesService.findSpace as jest.Mock).mockResolvedValue(spaceMock); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(profileSpaceMock); const response = controller.findOne( spaceMock.uuid, @@ -216,7 +207,7 @@ describe('SpacesControllerV2', () => { const response = controller.findOne(spaceMock.uuid, undefined, requestMock); await expect(response).rejects.toThrow(BadRequestException); - expect(profilesService.findProfileByProfileUuid).not.toHaveBeenCalled(); + expect(usersService.verifyUserProfile).not.toHaveBeenCalled(); }); it("findOne profile user doesn't have", async () => { @@ -227,9 +218,9 @@ describe('SpacesControllerV2', () => { userUuid: 'wrong user uuid', } as Profile; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); const response = controller.findOne( spaceMock.uuid, @@ -249,13 +240,11 @@ describe('SpacesControllerV2', () => { userUuid: requestMock.user.uuid, } as Profile; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest.spyOn(spacesService, 'findSpace').mockResolvedValue(spaceMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + (spacesService.findSpace as jest.Mock).mockResolvedValue(spaceMock); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(null); const response = controller.findOne( spaceMock.uuid, @@ -274,10 +263,8 @@ describe('SpacesControllerV2', () => { userUuid: requestMock.user.uuid, } as Profile; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest.spyOn(spacesService, 'findSpace').mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + (spacesService.findSpace as jest.Mock).mockResolvedValue(null); const response = controller.findOne( spaceMock.uuid, @@ -296,9 +283,9 @@ describe('SpacesControllerV2', () => { userUuid: requestMock.user.uuid, } as Profile; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); const response = controller.findOne( spaceMock.uuid, @@ -318,20 +305,18 @@ describe('SpacesControllerV2', () => { uuid: 'profile uuid', userUuid: requestMock.user.uuid, } as Profile; - const bodyMock = { name: 'new space name' } as UpdateSpaceRequestV2Dto; + const bodyMock = { name: 'new space name' } as UpdateSpaceRequestDto; const profileSpaceMock = { spaceUuid: spaceMock.uuid, profileUuid: profileMock.uuid, }; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(profileSpaceMock); - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue(iconUrlMock); - jest.spyOn(spacesService, 'updateSpace').mockResolvedValue(spaceMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(profileSpaceMock); + (uploadService.uploadFile as jest.Mock).mockResolvedValue(iconUrlMock); + (spacesService.updateSpace as jest.Mock).mockResolvedValue(spaceMock); const response = controller.update( iconMock, @@ -354,7 +339,7 @@ describe('SpacesControllerV2', () => { }); it('update icon not requested', async () => { - const bodyMock = { name: 'new space name' } as UpdateSpaceRequestV2Dto; + const bodyMock = { name: 'new space name' } as UpdateSpaceRequestDto; const spaceMock = { uuid: 'space uuid' } as Space; const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; const profileMock = { @@ -366,13 +351,11 @@ describe('SpacesControllerV2', () => { profileUuid: profileMock.uuid, }; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(profileSpaceMock); - jest.spyOn(spacesService, 'updateSpace').mockResolvedValue(spaceMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(profileSpaceMock); + (spacesService.updateSpace as jest.Mock).mockResolvedValue(spaceMock); const response = controller.update( null as unknown as Express.Multer.File, @@ -396,7 +379,7 @@ describe('SpacesControllerV2', () => { it('update name not requested', async () => { const iconMock = { filename: 'icon' } as Express.Multer.File; const iconUrlMock = 'www.test.com/image'; - const bodyMock = {} as UpdateSpaceRequestV2Dto; + const bodyMock = {} as UpdateSpaceRequestDto; const spaceMock = { uuid: 'space uuid' } as Space; const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; const profileMock = { @@ -408,14 +391,12 @@ describe('SpacesControllerV2', () => { profileUuid: profileMock.uuid, }; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(profileSpaceMock); - jest.spyOn(spacesService, 'updateSpace').mockResolvedValue(spaceMock); - jest.spyOn(uploadService, 'uploadFile').mockResolvedValue(iconUrlMock); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(profileSpaceMock); + (spacesService.updateSpace as jest.Mock).mockResolvedValue(spaceMock); + (uploadService.uploadFile as jest.Mock).mockResolvedValue(iconUrlMock); const response = controller.update( iconMock, @@ -444,11 +425,11 @@ describe('SpacesControllerV2', () => { uuid: 'profile uuid', userUuid: 'new user uuid', } as Profile; - const bodyMock = { name: 'new space name' } as UpdateSpaceRequestV2Dto; + const bodyMock = { name: 'new space name' } as UpdateSpaceRequestDto; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); const response = controller.update( iconMock, @@ -471,14 +452,12 @@ describe('SpacesControllerV2', () => { uuid: 'profile uuid', userUuid: requestMock.user.uuid, } as Profile; - const bodyMock = { name: 'new space name' } as UpdateSpaceRequestV2Dto; + const bodyMock = { name: 'new space name' } as UpdateSpaceRequestDto; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(profileMock); - jest - .spyOn(profileSpaceService, 'findProfileSpaceByBothUuid') - .mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockResolvedValue(true); + ( + profileSpaceService.findProfileSpaceByBothUuid as jest.Mock + ).mockResolvedValue(null); const response = controller.update( iconMock, @@ -501,11 +480,11 @@ describe('SpacesControllerV2', () => { uuid: 'profile uuid', userUuid: requestMock.user.uuid, } as Profile; - const bodyMock = { name: 'new space name' } as UpdateSpaceRequestV2Dto; + const bodyMock = { name: 'new space name' } as UpdateSpaceRequestDto; - jest - .spyOn(profilesService, 'findProfileByProfileUuid') - .mockResolvedValue(null); + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); const response = controller.update( iconMock, @@ -519,4 +498,81 @@ describe('SpacesControllerV2', () => { expect(uploadService.uploadFile).not.toHaveBeenCalled(); expect(spacesService.updateSpace).not.toHaveBeenCalled(); }); + + it('joinSpace', async () => { + const spaceMock = { uuid: 'space uuid' }; + const bodyMock = { profileUuid: 'profile uuid' }; + const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; + + (spacesService.joinSpace as jest.Mock).mockResolvedValue(spaceMock); + + const response = controller.joinSpace( + spaceMock.uuid, + bodyMock, + requestMock, + ); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.CREATED, + message: 'Created', + data: spaceMock, + }); + }); + + it('leaveSpace', async () => { + const spaceMock = { uuid: 'space uuid' }; + const profileMock = { uuid: 'profile uuid' }; + const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; + + (spacesService.leaveSpace as jest.Mock).mockResolvedValue(undefined); + + const response = controller.leaveSpace( + spaceMock.uuid, + profileMock.uuid, + requestMock, + ); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'OK', + }); + }); + + it('findProfilesInSpace', async () => { + const spaceMock = { uuid: 'space uuid' }; + const profileMock = { uuid: 'profile uuid' }; + const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; + const profilesMock = []; + + (spacesService.findProfilesInSpace as jest.Mock).mockResolvedValue( + profilesMock, + ); + + const response = controller.findProfilesInSpace( + spaceMock.uuid, + profileMock.uuid, + requestMock, + ); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'OK', + data: profilesMock, + }); + }); + + it('findProfilesInSpace space uuid needed', async () => { + const spaceMock = { uuid: 'space uuid' }; + const requestMock = { user: { uuid: 'user uuid' } } as RequestWithUser; + + (spacesService.findProfilesInSpace as jest.Mock).mockResolvedValue([]); + + const response = controller.findProfilesInSpace( + spaceMock.uuid, + undefined, + requestMock, + ); + + await expect(response).rejects.toThrow(BadRequestException); + }); }); diff --git a/nestjs-BE/server/src/spaces/spaces.controller.ts b/nestjs-BE/server/src/spaces/spaces.controller.ts index fbf7df6c..03b3b357 100644 --- a/nestjs-BE/server/src/spaces/spaces.controller.ts +++ b/nestjs-BE/server/src/spaces/spaces.controller.ts @@ -15,33 +15,29 @@ import { ForbiddenException, Query, BadRequestException, + Delete, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { SpacesService } from './spaces.service'; -import { - CreateSpaceRequestDto, - CreateSpaceRequestV2Dto, -} from './dto/create-space.dto'; -import { - UpdateSpaceRequestDto, - UpdateSpaceRequestV2Dto, -} from './dto/update-space.dto'; +import { CreateSpaceRequestDto } from './dto/create-space.dto'; +import { UpdateSpaceRequestDto } from './dto/update-space.dto'; import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; import { UploadService } from '../upload/upload.service'; import { ProfileSpaceService } from '../profile-space/profile-space.service'; import { RequestWithUser } from '../utils/interface'; -import { ProfilesService } from '../profiles/profiles.service'; import { ConfigService } from '@nestjs/config'; +import { UsersService } from '../users/users.service'; +import { JoinSpaceRequestDto } from './dto/join-space.dto'; -@Controller('v2/spaces') +@Controller('spaces') @ApiTags('spaces') -export class SpacesControllerV2 { +export class SpacesController { constructor( private readonly spacesService: SpacesService, private readonly uploadService: UploadService, private readonly profileSpaceService: ProfileSpaceService, - private readonly profilesService: ProfilesService, private readonly configService: ConfigService, + private readonly usersService: UsersService, ) {} @Post() @@ -76,23 +72,23 @@ export class SpacesControllerV2 { disableErrorMessages: true, }), ) - createSpaceDto: CreateSpaceRequestV2Dto, + createSpaceDto: CreateSpaceRequestDto, @Req() req: RequestWithUser, ) { if (!createSpaceDto.profileUuid) throw new BadRequestException(); - const profile = await this.profilesService.findProfileByProfileUuid( + await this.usersService.verifyUserProfile( + req.user.uuid, createSpaceDto.profileUuid, ); - if (!profile) throw new NotFoundException(); - if (req.user.uuid !== profile.userUuid) { - throw new ForbiddenException(); - } const iconUrl = icon ? await this.uploadService.uploadFile(icon) : this.configService.get('APP_ICON_URL'); createSpaceDto.icon = iconUrl; const space = await this.spacesService.createSpace(createSpaceDto); - await this.profileSpaceService.joinSpace(profile.uuid, space.uuid); + await this.profileSpaceService.joinSpace( + createSpaceDto.profileUuid, + space.uuid, + ); return { statusCode: HttpStatus.CREATED, message: 'Created', data: space }; } @@ -125,12 +121,7 @@ export class SpacesControllerV2 { @Req() req: RequestWithUser, ) { if (!profileUuid) throw new BadRequestException(); - const profile = - await this.profilesService.findProfileByProfileUuid(profileUuid); - if (!profile) throw new NotFoundException(); - if (req.user.uuid !== profile.userUuid) { - throw new ForbiddenException(); - } + await this.usersService.verifyUserProfile(req.user.uuid, profileUuid); const space = await this.spacesService.findSpace(spaceUuid); if (!space) throw new NotFoundException(); const profileSpace = @@ -170,16 +161,11 @@ export class SpacesControllerV2 { @Param('space_uuid') spaceUuid: string, @Query('profile_uuid') profileUuid: string, @Body(new ValidationPipe({ whitelist: true, disableErrorMessages: true })) - updateSpaceDto: UpdateSpaceRequestV2Dto, + updateSpaceDto: UpdateSpaceRequestDto, @Req() req: RequestWithUser, ) { if (!profileUuid) throw new BadRequestException(); - const profile = - await this.profilesService.findProfileByProfileUuid(profileUuid); - if (!profile) throw new NotFoundException(); - if (req.user.uuid !== profile.userUuid) { - throw new ForbiddenException(); - } + await this.usersService.verifyUserProfile(req.user.uuid, profileUuid); const profileSpace = await this.profileSpaceService.findProfileSpaceByBothUuid( profileUuid, @@ -196,91 +182,114 @@ export class SpacesControllerV2 { if (!space) throw new NotFoundException(); return { statusCode: HttpStatus.OK, message: 'OK', data: space }; } -} -/* - OLD VERSION -*/ -@Controller('spaces') -@ApiTags('spaces') -export class SpacesController { - constructor( - private readonly spacesService: SpacesService, - private readonly uploadService: UploadService, - private readonly profileSpaceService: ProfileSpaceService, - private readonly profilesService: ProfilesService, - private readonly configService: ConfigService, - ) {} - - @Post() - @UseInterceptors(FileInterceptor('icon')) - @ApiOperation({ summary: 'Create space' }) + @Post(':space_uuid/join') + @ApiOperation({ summary: 'Join space' }) @ApiResponse({ - status: 201, - description: 'The space has been successfully created.', + status: HttpStatus.CREATED, + description: 'Join data has been successfully created.', }) - async create( - @UploadedFile() icon: Express.Multer.File, - @Body(new ValidationPipe({ whitelist: true, disableErrorMessages: true })) - createSpaceDto: CreateSpaceRequestDto, + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Profile uuid needed.', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: 'User not logged in.', + }) + @ApiResponse({ + status: HttpStatus.FORBIDDEN, + description: 'Profile user not own.', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Profile not found.', + }) + @ApiResponse({ + status: HttpStatus.CONFLICT, + description: 'Conflict. You have already joined the space.', + }) + async joinSpace( + @Param('space_uuid') spaceUuid: string, + @Body( + new ValidationPipe({ + transform: true, + whitelist: true, + disableErrorMessages: true, + }), + ) + joinSpaceDto: JoinSpaceRequestDto, @Req() req: RequestWithUser, ) { - const profile = await this.profilesService.findProfile(req.user.uuid); - if (!profile) throw new NotFoundException(); - const iconUrl = icon - ? await this.uploadService.uploadFile(icon) - : this.configService.get('APP_ICON_URL'); - createSpaceDto.icon = iconUrl; - const space = await this.spacesService.createSpace(createSpaceDto); - await this.profileSpaceService.joinSpace(profile.uuid, space.uuid); - return { statusCode: 201, message: 'Created', data: space }; + const space = await this.spacesService.joinSpace( + req.user.uuid, + joinSpaceDto.profileUuid, + spaceUuid, + ); + return { statusCode: HttpStatus.CREATED, message: 'Created', data: space }; } - @Get(':space_uuid') - @ApiOperation({ summary: 'Get space by space_uuid' }) + @Delete(':space_uuid/profiles/:profile_uuid') + @ApiOperation({ summary: 'Leave space' }) @ApiResponse({ - status: 200, - description: 'Return the space data.', + status: HttpStatus.NO_CONTENT, + description: 'Successfully left the space.', }) @ApiResponse({ - status: 404, - description: 'Space not found.', + status: HttpStatus.UNAUTHORIZED, + description: 'User not logged in.', }) - async findOne(@Param('space_uuid') spaceUuid: string) { - const space = await this.spacesService.findSpace(spaceUuid); - if (!space) throw new NotFoundException(); - return { statusCode: 200, message: 'Success', data: space }; + @ApiResponse({ + status: HttpStatus.FORBIDDEN, + description: 'Profile user not own.', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Profile not found. Profile not joined space.', + }) + async leaveSpace( + @Param('space_uuid') spaceUuid: string, + @Param('profile_uuid') profileUuid: string, + @Req() req: RequestWithUser, + ) { + await this.spacesService.leaveSpace(req.user.uuid, profileUuid, spaceUuid); + return { statusCode: HttpStatus.OK, message: 'OK' }; } - @Patch(':space_uuid') - @UseInterceptors(FileInterceptor('icon')) - @ApiOperation({ summary: 'Update space by space_uuid' }) + @Get(':space_uuid/profiles') + @Header('Cache-Control', 'no-store') + @ApiOperation({ summary: 'Get profiles joined space.' }) @ApiResponse({ - status: 200, - description: 'Space has been successfully updated.', + status: HttpStatus.OK, + description: 'Successfully get profiles.', }) @ApiResponse({ - status: 400, - description: 'Bad Request. Invalid input data.', + status: HttpStatus.BAD_REQUEST, + description: 'Profile uuid needed.', }) @ApiResponse({ - status: 404, - description: 'Space not found.', + status: HttpStatus.UNAUTHORIZED, + description: 'User not logged in.', }) - async update( - @UploadedFile() icon: Express.Multer.File, + @ApiResponse({ + status: HttpStatus.FORBIDDEN, + description: 'Profile user not own. Profile not joined space.', + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Profile not found.', + }) + async findProfilesInSpace( @Param('space_uuid') spaceUuid: string, - @Body(new ValidationPipe({ whitelist: true, disableErrorMessages: true })) - updateSpaceDto: UpdateSpaceRequestDto, + @Query('profile_uuid') profileUuid: string, + @Req() req: RequestWithUser, ) { - if (icon) { - updateSpaceDto.icon = await this.uploadService.uploadFile(icon); - } - const space = await this.spacesService.updateSpace( + if (!profileUuid) throw new BadRequestException(); + const profiles = await this.spacesService.findProfilesInSpace( + req.user.uuid, + profileUuid, spaceUuid, - updateSpaceDto, ); - if (!space) throw new NotFoundException(); - return { statusCode: 200, message: 'Success', data: space }; + return { statusCode: HttpStatus.OK, message: 'OK', data: profiles }; } } diff --git a/nestjs-BE/server/src/spaces/spaces.module.ts b/nestjs-BE/server/src/spaces/spaces.module.ts index f3a866a7..8c00d20f 100644 --- a/nestjs-BE/server/src/spaces/spaces.module.ts +++ b/nestjs-BE/server/src/spaces/spaces.module.ts @@ -1,13 +1,13 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { SpacesService } from './spaces.service'; -import { SpacesController, SpacesControllerV2 } from './spaces.controller'; +import { SpacesController } from './spaces.controller'; import { UploadModule } from '../upload/upload.module'; import { ProfileSpaceModule } from '../profile-space/profile-space.module'; -import { ProfilesModule } from '../profiles/profiles.module'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [forwardRef(() => ProfileSpaceModule), ProfilesModule, UploadModule], - controllers: [SpacesController, SpacesControllerV2], + imports: [ProfileSpaceModule, UploadModule, UsersModule], + controllers: [SpacesController], providers: [SpacesService], exports: [SpacesService], }) diff --git a/nestjs-BE/server/src/spaces/spaces.service.spec.ts b/nestjs-BE/server/src/spaces/spaces.service.spec.ts index 4efe6e2a..fa7c1237 100644 --- a/nestjs-BE/server/src/spaces/spaces.service.spec.ts +++ b/nestjs-BE/server/src/spaces/spaces.service.spec.ts @@ -1,11 +1,21 @@ +import { + ConflictException, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SpacesService } from './spaces.service'; import { PrismaService } from '../prisma/prisma.service'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { ProfileSpaceService } from '../profile-space/profile-space.service'; +import { UsersService } from '../users/users.service'; +import { Space } from '@prisma/client'; describe('SpacesService', () => { let spacesService: SpacesService; let prisma: PrismaService; + let profileSpaceService: ProfileSpaceService; + let usersService: UsersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -13,19 +23,38 @@ describe('SpacesService', () => { SpacesService, { provide: PrismaService, - useValue: { space: { update: jest.fn() } }, + useValue: { + space: { update: jest.fn() }, + profile: { findMany: jest.fn() }, + }, + }, + { + provide: ProfileSpaceService, + useValue: { + createProfileSpace: jest.fn(), + deleteProfileSpace: jest.fn(), + isSpaceEmpty: jest.fn(), + isProfileInSpace: jest.fn(), + }, + }, + { + provide: UsersService, + useValue: { verifyUserProfile: jest.fn() }, }, ], }).compile(); spacesService = module.get(SpacesService); prisma = module.get(PrismaService); + profileSpaceService = module.get(ProfileSpaceService); + usersService = module.get(UsersService); }); it('updateSpace updated space', async () => { const data = { name: 'new space name', icon: 'new space icon' }; const spaceMock = { uuid: 'space uuid', ...data }; - jest.spyOn(prisma.space, 'update').mockResolvedValue(spaceMock); + + (prisma.space.update as jest.Mock).mockResolvedValue(spaceMock); const space = spacesService.updateSpace('space uuid', data); @@ -34,14 +63,13 @@ describe('SpacesService', () => { it('updateSpace fail', async () => { const data = { name: 'new space name', icon: 'new space icon' }; - jest - .spyOn(prisma.space, 'update') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'An operation failed because it depends on one or more records that were required but not found. Record to update not found.', - { code: 'P2025', clientVersion: '' }, - ), - ); + + (prisma.space.update as jest.Mock).mockRejectedValue( + new PrismaClientKnownRequestError('', { + code: 'P2025', + clientVersion: '', + }), + ); const space = spacesService.updateSpace('space uuid', data); @@ -50,10 +78,217 @@ describe('SpacesService', () => { it('updateSpace fail', async () => { const data = { name: 'new space name', icon: 'new space icon' }; - jest.spyOn(prisma.space, 'update').mockRejectedValue(new Error()); + + (prisma.space.update as jest.Mock).mockRejectedValue(new Error()); const space = spacesService.updateSpace('space uuid', data); await expect(space).rejects.toThrow(Error); }); + + it('joinSpace', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + const space = { uuid: spaceUuid } as Space; + + jest.spyOn(spacesService, 'findSpace').mockResolvedValue(space); + + const res = spacesService.joinSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).resolves.toEqual(space); + }); + + it('joinSpace profile not found', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); + + const res = spacesService.joinSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(NotFoundException); + }); + + it('joinSpace profile user not own', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); + + const res = spacesService.joinSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(ForbiddenException); + }); + + it('joinSpace conflict', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (profileSpaceService.createProfileSpace as jest.Mock).mockRejectedValue( + new PrismaClientKnownRequestError('', { + code: 'P2002', + clientVersion: '', + }), + ); + + const res = spacesService.joinSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(ConflictException); + }); + + it('joinSpace space not found', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (profileSpaceService.createProfileSpace as jest.Mock).mockRejectedValue( + new PrismaClientKnownRequestError('', { + code: 'P2003', + clientVersion: '', + }), + ); + + const res = spacesService.joinSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(ForbiddenException); + }); + + it('leaveSpace', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + jest.spyOn(spacesService, 'deleteSpace').mockResolvedValue(null); + + const res = spacesService.leaveSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).resolves.toBeUndefined(); + }); + + it('leaveSpace space delete fail', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + jest.spyOn(spacesService, 'deleteSpace').mockRejectedValue( + new PrismaClientKnownRequestError('', { + code: 'P2025', + clientVersion: '', + }), + ); + + const res = spacesService.leaveSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).resolves.toBeUndefined(); + }); + + it('leaveSpace profile not found', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); + jest.spyOn(spacesService, 'deleteSpace').mockResolvedValue(null); + + const res = spacesService.leaveSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(NotFoundException); + }); + + it('leaveSpace profile user not own', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); + jest.spyOn(spacesService, 'deleteSpace').mockResolvedValue(null); + + const res = spacesService.leaveSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(ForbiddenException); + }); + + it('leaveSpace profileSpace not found', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (profileSpaceService.deleteProfileSpace as jest.Mock).mockRejectedValue( + new PrismaClientKnownRequestError('', { + code: 'P2025', + clientVersion: '', + }), + ); + jest.spyOn(spacesService, 'deleteSpace').mockResolvedValue(null); + + const res = spacesService.leaveSpace(userUuid, profileUuid, spaceUuid); + + await expect(res).rejects.toThrow(NotFoundException); + }); + + it('findProfilesInSpace profile not found', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new NotFoundException(), + ); + + const res = spacesService.findProfilesInSpace( + userUuid, + profileUuid, + spaceUuid, + ); + + await expect(res).rejects.toThrow(NotFoundException); + }); + + it('findProfilesInSpace profile user not own', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (usersService.verifyUserProfile as jest.Mock).mockRejectedValue( + new ForbiddenException(), + ); + + const res = spacesService.findProfilesInSpace( + userUuid, + profileUuid, + spaceUuid, + ); + + await expect(res).rejects.toThrow(ForbiddenException); + }); + + it('findProfilesInSpace profile not joined space', async () => { + const userUuid = 'user uuid'; + const profileUuid = 'profile uuid'; + const spaceUuid = 'space uuid'; + + (profileSpaceService.isProfileInSpace as jest.Mock).mockResolvedValue( + false, + ); + + const res = spacesService.findProfilesInSpace( + userUuid, + profileUuid, + spaceUuid, + ); + + await expect(res).rejects.toThrow(ForbiddenException); + }); }); diff --git a/nestjs-BE/server/src/spaces/spaces.service.ts b/nestjs-BE/server/src/spaces/spaces.service.ts index e2b200a0..bef2ef57 100644 --- a/nestjs-BE/server/src/spaces/spaces.service.ts +++ b/nestjs-BE/server/src/spaces/spaces.service.ts @@ -1,13 +1,24 @@ -import { Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { UpdateSpacePrismaDto } from './dto/update-space.dto'; -import { Prisma, Space } from '@prisma/client'; +import { Prisma, Profile, Space } from '@prisma/client'; import { CreateSpacePrismaDto } from './dto/create-space.dto'; import { v4 as uuid } from 'uuid'; +import { ProfileSpaceService } from '../profile-space/profile-space.service'; +import { UsersService } from '../users/users.service'; @Injectable() export class SpacesService { - constructor(protected prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly profileSpaceService: ProfileSpaceService, + private readonly usersService: UsersService, + ) {} async findSpace(spaceUuid: string): Promise { return this.prisma.space.findUnique({ where: { uuid: spaceUuid } }); @@ -48,4 +59,71 @@ export class SpacesService { async deleteSpace(spaceUuid: string): Promise { return this.prisma.space.delete({ where: { uuid: spaceUuid } }); } + + async joinSpace( + userUuid: string, + profileUuid: string, + spaceUuid: string, + ): Promise { + await this.usersService.verifyUserProfile(userUuid, profileUuid); + try { + await this.profileSpaceService.createProfileSpace(profileUuid, spaceUuid); + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + switch (err.code) { + case 'P2002': + throw new ConflictException(); + case 'P2003': + throw new ForbiddenException(); + default: + throw err; + } + } else { + throw err; + } + } + return this.findSpace(spaceUuid); + } + + async leaveSpace( + userUuid: string, + profileUuid: string, + spaceUuid: string, + ): Promise { + await this.usersService.verifyUserProfile(userUuid, profileUuid); + try { + await this.profileSpaceService.deleteProfileSpace(profileUuid, spaceUuid); + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + switch (err.code) { + case 'P2025': + throw new NotFoundException(); + default: + throw err; + } + } else { + throw err; + } + } + const isSpaceEmpty = await this.profileSpaceService.isSpaceEmpty(spaceUuid); + try { + if (!isSpaceEmpty) await this.deleteSpace(spaceUuid); + } catch (err) {} + } + + async findProfilesInSpace( + userUuid: string, + profileUuid: string, + spaceUuid: string, + ): Promise { + await this.usersService.verifyUserProfile(userUuid, profileUuid); + const isProfileInSpace = await this.profileSpaceService.isProfileInSpace( + profileUuid, + spaceUuid, + ); + if (!isProfileInSpace) throw new ForbiddenException(); + return this.prisma.profile.findMany({ + where: { spaces: { some: { spaceUuid } } }, + }); + } } diff --git a/nestjs-BE/server/src/users/users.controller.spec.ts b/nestjs-BE/server/src/users/users.controller.spec.ts new file mode 100644 index 00000000..ea8344b5 --- /dev/null +++ b/nestjs-BE/server/src/users/users.controller.spec.ts @@ -0,0 +1,42 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { HttpStatus } from '@nestjs/common'; +import { RequestWithUser } from '../utils/interface'; + +describe('UsersController', () => { + let controller: UsersController; + let usersService: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: { findUserJoinedSpaces: jest.fn() }, + }, + ], + }).compile(); + + controller = module.get(UsersController); + usersService = module.get(UsersService); + }); + + it('findUserJoinedSpaces', async () => { + const reqMock = { user: { uuid: 'user uuid' } } as RequestWithUser; + const spacesMock = []; + + (usersService.findUserJoinedSpaces as jest.Mock).mockResolvedValue( + spacesMock, + ); + + const response = controller.findUserJoinedSpaces(reqMock); + + await expect(response).resolves.toEqual({ + statusCode: HttpStatus.OK, + message: 'OK', + data: spacesMock, + }); + }); +}); diff --git a/nestjs-BE/server/src/users/users.controller.ts b/nestjs-BE/server/src/users/users.controller.ts new file mode 100644 index 00000000..a2256192 --- /dev/null +++ b/nestjs-BE/server/src/users/users.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, HttpStatus, Req } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { UsersService } from './users.service'; +import { RequestWithUser } from '../utils/interface'; + +@Controller('users') +@ApiTags('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Get('spaces') + @ApiOperation({ summary: 'Get spaces user joined.' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Spaces found.', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: 'User not logged in.', + }) + async findUserJoinedSpaces(@Req() req: RequestWithUser) { + const spaces = await this.usersService.findUserJoinedSpaces(req.user.uuid); + + return { statusCode: HttpStatus.OK, message: 'OK', data: spaces }; + } +} diff --git a/nestjs-BE/server/src/users/users.module.ts b/nestjs-BE/server/src/users/users.module.ts index 8fa904f1..6fd5b2ad 100644 --- a/nestjs-BE/server/src/users/users.module.ts +++ b/nestjs-BE/server/src/users/users.module.ts @@ -1,7 +1,12 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { ProfilesModule } from '../profiles/profiles.module'; @Module({ + imports: [PrismaModule, ProfilesModule], + controllers: [UsersController], providers: [UsersService], exports: [UsersService], }) diff --git a/nestjs-BE/server/src/users/users.service.spec.ts b/nestjs-BE/server/src/users/users.service.spec.ts index 81f58428..c98fbd32 100644 --- a/nestjs-BE/server/src/users/users.service.spec.ts +++ b/nestjs-BE/server/src/users/users.service.spec.ts @@ -3,15 +3,22 @@ import { UsersService } from './users.service'; import { PrismaService } from '../prisma/prisma.service'; import { v4 as uuid } from 'uuid'; import { KakaoUser, User } from '@prisma/client'; +import { ProfilesService } from '../profiles/profiles.service'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; describe('UsersService', () => { let usersService: UsersService; + let profilesService: ProfilesService; let prisma: PrismaService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, + { + provide: ProfilesService, + useValue: { findProfileByProfileUuid: jest.fn() }, + }, { provide: PrismaService, useValue: { @@ -24,6 +31,7 @@ describe('UsersService', () => { }).compile(); usersService = module.get(UsersService); + profilesService = module.get(ProfilesService); prisma = module.get(PrismaService); }); @@ -69,4 +77,43 @@ describe('UsersService', () => { expect(prisma.user.create).toHaveBeenCalled(); expect(prisma.user.findUnique).not.toHaveBeenCalled(); }); + + it('verifyUserProfile verified', async () => { + const userMock = { uuid: 'user uuid' }; + const profileMock = { uuid: 'profile uuid', userUuid: userMock.uuid }; + + (profilesService.findProfileByProfileUuid as jest.Mock).mockResolvedValue( + profileMock, + ); + + const res = usersService.verifyUserProfile(userMock.uuid, profileMock.uuid); + + await expect(res).resolves.toBeTruthy(); + }); + + it('verifyUserProfile profile not found', async () => { + const userMock = { uuid: 'user uuid' }; + const profileMock = { uuid: 'profile uuid', userUuid: userMock.uuid }; + + (profilesService.findProfileByProfileUuid as jest.Mock).mockResolvedValue( + null, + ); + + const res = usersService.verifyUserProfile(userMock.uuid, profileMock.uuid); + + await expect(res).rejects.toThrow(NotFoundException); + }); + + it('verifyUserProfile profile user not own', async () => { + const userMock = { uuid: 'user uuid' }; + const profileMock = { uuid: 'profile uuid', userUuid: 'other user uuid' }; + + (profilesService.findProfileByProfileUuid as jest.Mock).mockResolvedValue( + profileMock, + ); + + const res = usersService.verifyUserProfile(userMock.uuid, profileMock.uuid); + + await expect(res).rejects.toThrow(ForbiddenException); + }); }); diff --git a/nestjs-BE/server/src/users/users.service.ts b/nestjs-BE/server/src/users/users.service.ts index 72f28a0a..133c2e9d 100644 --- a/nestjs-BE/server/src/users/users.service.ts +++ b/nestjs-BE/server/src/users/users.service.ts @@ -1,12 +1,20 @@ -import { Injectable } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserPrismaDto } from './dto/create-user.dto'; -import { User } from '@prisma/client'; +import { Space, User } from '@prisma/client'; import { v4 as uuid } from 'uuid'; +import { ProfilesService } from '../profiles/profiles.service'; @Injectable() export class UsersService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly profilesService: ProfilesService, + ) {} async getOrCreateUser(data: CreateUserPrismaDto): Promise { return this.prisma.$transaction(async () => { @@ -35,4 +43,23 @@ export class UsersService { return user; }); } + + async findUserJoinedSpaces(userUuid: string): Promise { + const spaces = await this.prisma.space.findMany({ + where: { profileSpaces: { some: { profile: { userUuid } } } }, + }); + + return spaces; + } + + async verifyUserProfile( + userUuid: string, + profileUuid: string, + ): Promise { + const profile = + await this.profilesService.findProfileByProfileUuid(profileUuid); + if (!profile) throw new NotFoundException(); + if (userUuid !== profile.userUuid) throw new ForbiddenException(); + return true; + } } diff --git a/nestjs-BE/server/test/spaces.e2e-spec.ts b/nestjs-BE/server/test/spaces.e2e-spec.ts index 9792462f..1a0882b4 100644 --- a/nestjs-BE/server/test/spaces.e2e-spec.ts +++ b/nestjs-BE/server/test/spaces.e2e-spec.ts @@ -75,7 +75,7 @@ describe('SpacesController (e2e)', () => { await app.close(); }); - it('/v2/spaces (POST)', () => { + it('/spaces (POST)', () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -90,7 +90,7 @@ describe('SpacesController (e2e)', () => { const imageRegExp = new RegExp(imageUrlPattern); return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .field('profile_uuid', testProfile.uuid) @@ -107,11 +107,11 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces (POST) without space image', () => { + it('/spaces (POST) without space image', () => { const newSpace = { name: 'new test space' }; return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .send({ name: newSpace.name, profile_uuid: testProfile.uuid }) .expect(HttpStatus.CREATED) @@ -128,7 +128,7 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces (POST) without profile uuid', () => { + it('/spaces (POST) without profile uuid', () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -136,7 +136,7 @@ describe('SpacesController (e2e)', () => { }; return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -144,14 +144,14 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Bad Request', statusCode: HttpStatus.BAD_REQUEST }); }); - it('/v2/spaces (POST) without space name', () => { + it('/spaces (POST) without space name', () => { const newSpace = { icon: testImagePath, iconContentType: 'image/png', }; return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .field('profile_uuid', testProfile.uuid) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -159,14 +159,14 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Bad Request', statusCode: HttpStatus.BAD_REQUEST }); }); - it('/v2/spaces (POST) not logged in', () => { + it('/spaces (POST) not logged in', () => { return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .expect(HttpStatus.UNAUTHORIZED) .expect({ message: 'Unauthorized', statusCode: HttpStatus.UNAUTHORIZED }); }); - it("/v2/spaces (POST) profile user doesn't have", async () => { + it("/spaces (POST) profile user doesn't have", async () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -183,7 +183,7 @@ describe('SpacesController (e2e)', () => { }); return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .field('profile_uuid', newProfile.uuid) @@ -192,7 +192,7 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); }); - it('/v2/spaces (POST) profilie not found', () => { + it('/spaces (POST) profilie not found', () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -200,7 +200,7 @@ describe('SpacesController (e2e)', () => { }; return request(app.getHttpServer()) - .post('/v2/spaces') + .post('/spaces') .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .field('profile_uuid', uuid()) @@ -209,13 +209,13 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) space found', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) space found', async () => { await prisma.profileSpace.create({ data: { spaceUuid: testSpace.uuid, profileUuid: testProfile.uuid }, }); return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .get(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.OK) .expect({ @@ -225,26 +225,26 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) query profile_uuid needed', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) query profile_uuid needed', async () => { return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}`) + .get(`/spaces/${testSpace.uuid}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.BAD_REQUEST) .expect({ message: 'Bad Request', statusCode: HttpStatus.BAD_REQUEST }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) not logged in', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) not logged in', async () => { await prisma.profileSpace.create({ data: { spaceUuid: testSpace.uuid, profileUuid: testProfile.uuid }, }); return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .get(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .expect(HttpStatus.UNAUTHORIZED) .expect({ message: 'Unauthorized', statusCode: HttpStatus.UNAUTHORIZED }); }); - it("/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) profile user doesn't have", async () => { + it("/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) profile user doesn't have", async () => { const newUser = await prisma.user.create({ data: { uuid: uuid() } }); const newProfile = await prisma.profile.create({ data: { @@ -259,37 +259,37 @@ describe('SpacesController (e2e)', () => { }); return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) + .get(`/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.FORBIDDEN) .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) profile not existing', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) profile not existing', async () => { return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}?profile_uuid=${uuid()}`) + .get(`/spaces/${testSpace.uuid}?profile_uuid=${uuid()}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.NOT_FOUND) .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) findOne profile not joined space', () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) findOne profile not joined space', () => { return request(app.getHttpServer()) - .get(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .get(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.FORBIDDEN) .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) not existing space', () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (GET) not existing space', () => { return request(app.getHttpServer()) - .get(`/v2/spaces/${uuid()}?profile_uuid=${testProfile.uuid}`) + .get(`/spaces/${uuid()}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .expect(HttpStatus.NOT_FOUND) .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) update success', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) update success', async () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -302,11 +302,11 @@ describe('SpacesController (e2e)', () => { 'S3_BUCKET_NAME', )}\\.s3\\.${configService.get( 'AWS_REGION', - )}\\.amazonaws\\.com\\/[0-9a-f]{32}-`; + )}\\.amazonaws\\.com\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-`; const imageRegExp = new RegExp(imageUrlPattern); return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -320,7 +320,7 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) request without name', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) request without name', async () => { const newSpace = { icon: testImagePath, iconContentType: 'image/png', @@ -329,14 +329,14 @@ describe('SpacesController (e2e)', () => { 'S3_BUCKET_NAME', )}\\.s3\\.${configService.get( 'AWS_REGION', - )}\\.amazonaws\\.com\\/[0-9a-f]{32}-`; + )}\\.amazonaws\\.com\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-`; const imageRegExp = new RegExp(imageUrlPattern); await prisma.profileSpace.create({ data: { spaceUuid: testSpace.uuid, profileUuid: testProfile.uuid }, }); return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) .expect(HttpStatus.OK) @@ -349,14 +349,14 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) request without icon', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) request without icon', async () => { const newSpace = { name: 'new test space' }; await prisma.profileSpace.create({ data: { spaceUuid: testSpace.uuid, profileUuid: testProfile.uuid }, }); return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .send({ name: newSpace.name }) .expect(HttpStatus.OK) @@ -371,7 +371,7 @@ describe('SpacesController (e2e)', () => { }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile uuid needed', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile uuid needed', async () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -379,7 +379,7 @@ describe('SpacesController (e2e)', () => { }; return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}`) + .patch(`/spaces/${testSpace.uuid}`) .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -387,12 +387,12 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Bad Request', statusCode: HttpStatus.BAD_REQUEST }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) unauthorized', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) unauthorized', async () => { const icon = await readFile(resolve(__dirname, './base_image.png')); const newSpace = { name: 'new test space', icon }; return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${testProfile.uuid}`) .field('name', newSpace.name) .attach('icon', newSpace.icon) .expect(HttpStatus.UNAUTHORIZED) @@ -402,7 +402,7 @@ describe('SpacesController (e2e)', () => { }); }); - it("/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile user doesn't have", async () => { + it("/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile user doesn't have", async () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -422,7 +422,7 @@ describe('SpacesController (e2e)', () => { }); return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -430,7 +430,7 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile not joined space', async () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile not joined space', async () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -447,7 +447,7 @@ describe('SpacesController (e2e)', () => { }); return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${newProfile.uuid}`) .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) @@ -455,7 +455,7 @@ describe('SpacesController (e2e)', () => { .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); }); - it('/v2/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile not found', () => { + it('/spaces/:space_uuid?profile_uuid={profile_uuid} (PATCH) profile not found', () => { const newSpace = { name: 'new test space', icon: testImagePath, @@ -463,11 +463,273 @@ describe('SpacesController (e2e)', () => { }; return request(app.getHttpServer()) - .patch(`/v2/spaces/${testSpace.uuid}?profile_uuid=${uuid()}`) + .patch(`/spaces/${testSpace.uuid}?profile_uuid=${uuid()}`) .auth(testToken, { type: 'bearer' }) .field('name', newSpace.name) .attach('icon', newSpace.icon, { contentType: newSpace.iconContentType }) .expect(HttpStatus.NOT_FOUND) .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); }); + + it('/spaces/:space_uuid/join (POST)', async () => { + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: testProfile.uuid }) + .expect(HttpStatus.CREATED) + .expect({ + message: 'Created', + statusCode: HttpStatus.CREATED, + data: testSpace, + }); + }); + + it('/spaces/:space_uuid/join (POST) profile uuid needed', async () => { + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.BAD_REQUEST) + .expect({ + message: 'Bad Request', + statusCode: HttpStatus.BAD_REQUEST, + }); + }); + + it('/spaces/:space_uuid/join (POST) profile uuid wrong type', async () => { + const number = 1; + + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: number }) + .expect(HttpStatus.BAD_REQUEST) + .expect({ + message: 'Bad Request', + statusCode: HttpStatus.BAD_REQUEST, + }); + }); + + it('/spaces/:space_uuid/join (POST) user not logged in', async () => { + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .send({ profile_uuid: testProfile.uuid }) + .expect(HttpStatus.UNAUTHORIZED) + .expect({ + message: 'Unauthorized', + statusCode: HttpStatus.UNAUTHORIZED, + }); + }); + + it('/spaces/:space_uuid/join (POST) profile user not own', async () => { + const newUser = await prisma.user.create({ data: { uuid: uuid() } }); + const newProfile = await prisma.profile.create({ + data: { + uuid: uuid(), + userUuid: newUser.uuid, + image: 'test image', + nickname: 'test nickname', + }, + }); + + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: newProfile.uuid }) + .expect(HttpStatus.FORBIDDEN) + .expect({ + message: 'Forbidden', + statusCode: HttpStatus.FORBIDDEN, + }); + }); + + it('/spaces/:space_uuid/join (POST) space not exist', async () => { + return request(app.getHttpServer()) + .post(`/spaces/${uuid()}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: testProfile.uuid }) + .expect(HttpStatus.FORBIDDEN) + .expect({ + message: 'Forbidden', + statusCode: HttpStatus.FORBIDDEN, + }); + }); + + it('/spaces/:space_uuid/join (POST) profile not found', async () => { + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: uuid() }) + .expect(HttpStatus.NOT_FOUND) + .expect({ + message: 'Not Found', + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + it('/spaces/:space_uuid/join (POST) already joined space', async () => { + await prisma.profileSpace.create({ + data: { + spaceUuid: testSpace.uuid, + profileUuid: testProfile.uuid, + }, + }); + + return request(app.getHttpServer()) + .post(`/spaces/${testSpace.uuid}/join`) + .auth(testToken, { type: 'bearer' }) + .send({ profile_uuid: testProfile.uuid }) + .expect(HttpStatus.CONFLICT) + .expect({ + message: 'Conflict', + statusCode: HttpStatus.CONFLICT, + }); + }); + + it('/spaces/:space_uuid/profiles/:profile_uuid (DELETE)', async () => { + await prisma.profileSpace.create({ + data: { + profileUuid: testProfile.uuid, + spaceUuid: testSpace.uuid, + }, + }); + + return request(app.getHttpServer()) + .delete(`/spaces/${testSpace.uuid}/profiles/${testProfile.uuid}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.OK) + .expect({ message: 'OK', statusCode: HttpStatus.OK }); + }); + + it('/spaces/:space_uuid/profiles/:profile_uuid (DELETE) user not logged in', async () => { + await prisma.profileSpace.create({ + data: { + profileUuid: testProfile.uuid, + spaceUuid: testSpace.uuid, + }, + }); + + return request(app.getHttpServer()) + .delete(`/spaces/${testSpace.uuid}/profiles/${testProfile.uuid}`) + .expect(HttpStatus.UNAUTHORIZED) + .expect({ message: 'Unauthorized', statusCode: HttpStatus.UNAUTHORIZED }); + }); + + it('/spaces/:space_uuid/profiles/:profile_uuid (DELETE) profile user not own', async () => { + const newUser = await prisma.user.create({ data: { uuid: uuid() } }); + const newProfile = await prisma.profile.create({ + data: { + uuid: uuid(), + userUuid: newUser.uuid, + image: 'test image', + nickname: 'test nickname', + }, + }); + await prisma.profileSpace.create({ + data: { + profileUuid: testProfile.uuid, + spaceUuid: testSpace.uuid, + }, + }); + + return request(app.getHttpServer()) + .delete(`/spaces/${testSpace.uuid}/profiles/${newProfile.uuid}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.FORBIDDEN) + .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); + }); + + it('/spaces/:space_uuid/profiles/:profile_uuid (DELETE) profile user not own', async () => { + await prisma.profileSpace.create({ + data: { + profileUuid: testProfile.uuid, + spaceUuid: testSpace.uuid, + }, + }); + + return request(app.getHttpServer()) + .delete(`/spaces/${testSpace.uuid}/profiles/${uuid()}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.NOT_FOUND) + .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); + }); + + it('/spaces/:space_uuid/profiles/:profile_uuid (DELETE) profile user not own', async () => { + return request(app.getHttpServer()) + .delete(`/spaces/${testSpace.uuid}/profiles/${testProfile.uuid}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.NOT_FOUND) + .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); + }); + + it('/spaces/:space_uuid/profiles (GET)', async () => { + await prisma.profileSpace.create({ + data: { + profileUuid: testProfile.uuid, + spaceUuid: testSpace.uuid, + }, + }); + + return request(app.getHttpServer()) + .get( + `/spaces/${testSpace.uuid}/profiles?profile_uuid=${testProfile.uuid}`, + ) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.OK) + .expect((res) => { + expect(res.body.message).toBe('OK'); + expect(res.body.statusCode).toBe(HttpStatus.OK); + expect(res.body.data).toEqual(expect.arrayContaining([testProfile])); + }); + }); + + it('/spaces/:space_uuid/profiles (GET) profile uuid needed', async () => { + return request(app.getHttpServer()) + .get(`/spaces/${testSpace.uuid}/profiles`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.BAD_REQUEST) + .expect({ message: 'Bad Request', statusCode: HttpStatus.BAD_REQUEST }); + }); + + it('/spaces/:space_uuid/profiles (GET) user not logged in', async () => { + return request(app.getHttpServer()) + .get(`/spaces/${testSpace.uuid}/profiles`) + .expect(HttpStatus.UNAUTHORIZED) + .expect({ message: 'Unauthorized', statusCode: HttpStatus.UNAUTHORIZED }); + }); + + it('/spaces/:space_uuid/profiles (GET) profile user not own', async () => { + const newUser = await prisma.user.create({ data: { uuid: uuid() } }); + const newProfile = await prisma.profile.create({ + data: { + uuid: uuid(), + userUuid: newUser.uuid, + image: 'test image', + nickname: 'test nickname', + }, + }); + + return request(app.getHttpServer()) + .get(`/spaces/${testSpace.uuid}/profiles?profile_uuid=${newProfile.uuid}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.FORBIDDEN) + .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); + }); + + it('/spaces/:space_uuid/profiles (GET) profile not joined space', async () => { + return request(app.getHttpServer()) + .get( + `/spaces/${testSpace.uuid}/profiles?profile_uuid=${testProfile.uuid}`, + ) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.FORBIDDEN) + .expect({ message: 'Forbidden', statusCode: HttpStatus.FORBIDDEN }); + }); + + it('/spaces/:space_uuid/profiles (GET) profile not found', async () => { + return request(app.getHttpServer()) + .get(`/spaces/${testSpace.uuid}/profiles?profile_uuid=${uuid()}`) + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.NOT_FOUND) + .expect({ message: 'Not Found', statusCode: HttpStatus.NOT_FOUND }); + }); }); diff --git a/nestjs-BE/server/test/users.e2e-spec.ts b/nestjs-BE/server/test/users.e2e-spec.ts new file mode 100644 index 00000000..c3d1955f --- /dev/null +++ b/nestjs-BE/server/test/users.e2e-spec.ts @@ -0,0 +1,105 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { User } from '@prisma/client'; +import * as request from 'supertest'; +import { v4 as uuid } from 'uuid'; +import { sign } from 'jsonwebtoken'; +import { PrismaService } from '../src/prisma/prisma.service'; +import { UsersModule } from '../src/users/users.module'; +import { AuthModule } from '../src/auth/auth.module'; + +describe('UsersController (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + let configService: ConfigService; + let testToken: string; + let testUser: User; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + UsersModule, + AuthModule, + ], + }).compile(); + + app = module.createNestApplication(); + + await app.init(); + + prisma = module.get(PrismaService); + configService = module.get(ConfigService); + + await prisma.user.deleteMany({}); + + testUser = await prisma.user.create({ data: { uuid: uuid() } }); + testToken = sign( + { sub: testUser.uuid }, + configService.get('JWT_ACCESS_SECRET'), + { expiresIn: '5m' }, + ); + }); + + beforeEach(async () => { + await prisma.profile.deleteMany({}); + await prisma.space.deleteMany({}); + await prisma.profileSpace.deleteMany({}); + }); + + afterAll(async () => { + await app.close(); + }); + + it('users/spaces (GET)', async () => { + const SPACE_NUMBER = 5; + + const profile = await prisma.profile.create({ + data: { + uuid: uuid(), + userUuid: testUser.uuid, + image: 'test image', + nickname: 'test nickname', + }, + }); + const spacePromises = Array.from({ length: SPACE_NUMBER }, async () => { + const space = await prisma.space.create({ + data: { uuid: uuid(), name: 'test space', icon: 'test icon' }, + }); + await prisma.profileSpace.create({ + data: { + profileUuid: profile.uuid, + spaceUuid: space.uuid, + }, + }); + return space; + }); + const spaces = await Promise.all(spacePromises); + + return request(app.getHttpServer()) + .get('/users/spaces') + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.OK) + .expect((res) => { + expect(res.body.statusCode).toBe(HttpStatus.OK); + expect(res.body.message).toBe('OK'); + expect(res.body.data).toEqual(expect.arrayContaining(spaces)); + }); + }); + + it('users/spaces (GET) no joined spaces', async () => { + return request(app.getHttpServer()) + .get('/users/spaces') + .auth(testToken, { type: 'bearer' }) + .expect(HttpStatus.OK) + .expect({ statusCode: HttpStatus.OK, message: 'OK', data: [] }); + }); + + it('users/spaces (GET)', async () => { + return request(app.getHttpServer()) + .get('/users/spaces') + .expect(HttpStatus.UNAUTHORIZED) + .expect({ statusCode: HttpStatus.UNAUTHORIZED, message: 'Unauthorized' }); + }); +});