diff --git a/backend/src/room/dto/create-room.dto.ts b/backend/src/room/dto/create-room.dto.ts new file mode 100644 index 00000000..130607e5 --- /dev/null +++ b/backend/src/room/dto/create-room.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateRoomDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + name: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + description: string; + + // @ApiProperty() + // @IsNotEmpty() + // @IsString() + // code: string; +} diff --git a/backend/src/room/dto/update-room.dto.ts b/backend/src/room/dto/update-room.dto.ts new file mode 100644 index 00000000..ab4e6c2b --- /dev/null +++ b/backend/src/room/dto/update-room.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRoomDto } from './create-room.dto'; + +export class UpdateRoomDto extends PartialType(CreateRoomDto) {} \ No newline at end of file diff --git a/backend/src/room/entities/room.entity.ts b/backend/src/room/entities/room.entity.ts new file mode 100644 index 00000000..d5320722 --- /dev/null +++ b/backend/src/room/entities/room.entity.ts @@ -0,0 +1,22 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('rooms') +export class Room { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column('text') + description: string; + + @Column() + code: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/backend/src/room/room.controller.spec.ts b/backend/src/room/room.controller.spec.ts new file mode 100644 index 00000000..17d32ee3 --- /dev/null +++ b/backend/src/room/room.controller.spec.ts @@ -0,0 +1,128 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoomController } from './room.controller'; +import { RoomService } from './room.service'; +import { CreateRoomDto } from './dto/create-room.dto'; +import { UpdateRoomDto } from './dto/update-room.dto'; +import { NotFoundException } from '@nestjs/common'; + +describe('RoomController', () => { + let controller: RoomController; + let service: RoomService; + + const mockRoomService = { + create: jest.fn().mockResolvedValue({ + id: '1', + name: 'Test Room Name', + code: 'RANDOM', + description: 'Test Room Description', + }), + // create: jest.fn().mockImplementation((dto) => ({ id: '1', ...dto })), + findAll: jest.fn().mockResolvedValue([]), + findOne: jest.fn().mockImplementation((id) => { + if (id === '1') + return Promise.resolve({ + id: '1', + name: 'Test Room Name', + description: 'Test Room Description', + code: 'RANDOM', + }); + throw new NotFoundException('Room not found'); + }), + update: jest.fn().mockImplementation((id, dto) => { + if (id === '1') return Promise.resolve({ id: '1', ...dto }); + throw new NotFoundException('Room not found'); + }), + remove: jest.fn().mockImplementation((id) => { + if (id === '1') return Promise.resolve(true); + throw new NotFoundException('Room not found'); + }), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RoomController], + providers: [{ provide: RoomService, useValue: mockRoomService }], + }).compile(); + + controller = module.get(RoomController); + service = module.get(RoomService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should create a room', async () => { + const dto: CreateRoomDto = { + name: 'Test Room Name', + description: 'Test Room Description', + }; + await expect(controller.create(dto)).resolves.toEqual({ + id: '1', + name: 'Test Room Name', + code: 'RANDOM', + description: 'Test Room Description', + }); + expect(service.create).toHaveBeenCalledWith(dto); + }); + + it('should return all rooms', async () => { + await expect(controller.findAll()).resolves.toEqual([]); + expect(service.findAll).toHaveBeenCalled(); + }); + + it('should return a room by id', async () => { + await expect(controller.findOne('1')).resolves.toEqual({ + id: '1', + name: 'Test Room Name', + description: 'Test Room Description', + code: 'RANDOM', + }); + expect(service.findOne).toHaveBeenCalledWith('1'); + }); + + it('should throw an error if room not found', async () => { + try { + await controller.findOne('2'); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + } + }); + + it('should update a room', async () => { + const updateDto: UpdateRoomDto = { + name: 'Updated Room', + description: 'Test Room Description' + }; + await expect(controller.update('1', updateDto)).resolves.toEqual({ + id: '1', + ...updateDto, + }); + expect(service.update).toHaveBeenCalledWith('1', updateDto); + }); + + it('should throw an error if updating a non-existent room', async () => { + const updateDto: UpdateRoomDto = { + name: 'Updated Room', + description: 'Test Room Description' + }; + try { + await expect(controller.update('2', updateDto)); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + } + }); + + it('should delete a room', async () => { + await expect(controller.remove('1')).resolves.toBeTruthy(); + expect(service.remove).toHaveBeenCalledWith('1'); + }); + + it('should throw an error if deleting a non-existent room', async () => { + try { + await expect(controller.remove('2')); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/backend/src/room/room.controller.ts b/backend/src/room/room.controller.ts new file mode 100644 index 00000000..c82532fe --- /dev/null +++ b/backend/src/room/room.controller.ts @@ -0,0 +1,103 @@ +// src/room/room.controller.ts +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiParam, + ApiBody, +} from '@nestjs/swagger'; +import { RoomService } from './room.service'; +import { CreateRoomDto } from './dto/create-room.dto'; +import { UpdateRoomDto } from './dto/update-room.dto'; + +@ApiTags('room') +@Controller('room') +export class RoomController { + constructor(private readonly roomService: RoomService) {} + + @Post() + @ApiOperation({ summary: 'Create a new room' }) + @ApiBody({ type: CreateRoomDto }) + @ApiResponse({ + status: 201, + description: 'Room created successfully', + }) + @ApiResponse({ + status: 400, + description: 'Invalid input', + }) + create(@Body() createRoomDto: CreateRoomDto) { + return this.roomService.create(createRoomDto); + } + + @Get() + @ApiOperation({ summary: 'Get all room' }) + @ApiResponse({ + status: 200, + description: 'List of all room successfully retrieved', + }) + findAll() { + return this.roomService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a room by id' }) + findOne(@Param('id') id: string) { + return this.roomService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ + summary: 'Update a room', + description: 'Update an existing room by its ID', + }) + @ApiParam({ + name: 'id', + description: 'Unique identifier of the room to be updated', + type: 'string', + }) + @ApiBody({ type: UpdateRoomDto }) + @ApiResponse({ + status: 200, + description: 'Room successfully updated', + }) + @ApiResponse({ + status: 404, + description: 'Room not found', + }) + update(@Param('id') id: string, @Body() updateRoomDto: UpdateRoomDto) { + return this.roomService.update(id, updateRoomDto); + } + + @Delete(':id') + @ApiOperation({ + summary: 'Delete a room', + description: 'Delete a room from the collection by its ID', + }) + @ApiParam({ + name: 'id', + description: 'Unique identifier of the room to be deleted', + type: 'string', + }) + @ApiResponse({ + status: 200, + description: 'Room successfully deleted', + }) + @ApiResponse({ + status: 404, + description: 'Room not found', + }) + remove(@Param('id') id: string) { + return this.roomService.remove(id); + } +} diff --git a/backend/src/room/room.module.ts b/backend/src/room/room.module.ts new file mode 100644 index 00000000..8bec91e7 --- /dev/null +++ b/backend/src/room/room.module.ts @@ -0,0 +1,14 @@ +// src/room/room.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RoomService } from './room.service'; +import { RoomController } from './room.controller'; +import { Room } from './entities/room.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Room])], + controllers: [RoomController], + providers: [RoomService], + exports: [RoomService], +}) +export class RoomModule {} \ No newline at end of file diff --git a/backend/src/room/room.service.spec.ts b/backend/src/room/room.service.spec.ts new file mode 100644 index 00000000..7c98e45d --- /dev/null +++ b/backend/src/room/room.service.spec.ts @@ -0,0 +1,105 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoomService } from './room.service'; +import { Repository } from 'typeorm'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Room } from './entities/room.entity'; +import { NotFoundException } from '@nestjs/common'; + +const mockRoomRepository = () => ({ + create: jest.fn().mockImplementation((dto) => ({ ...dto, code: 'RANDOM' })), + save: jest + .fn() + .mockImplementation((room) => Promise.resolve({ id: '1', ...room })), + find: jest.fn().mockResolvedValue([]), + findOne: jest + .fn() + .mockImplementation(({ where: { id } }) => + Promise.resolve( + id === '1' + ? { + id: '1', + name: 'Test Room Name', + description: 'Test Room Description', + code: 'RANDOM', + } + : null, + ), + ), + remove: jest.fn().mockResolvedValue(true), +}); + +describe('RoomService', () => { + let service: RoomService; + let repository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RoomService, + { + provide: getRepositoryToken(Room), + useValue: mockRoomRepository(), + }, + ], + }).compile(); + + service = module.get(RoomService); + repository = module.get>(getRepositoryToken(Room)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create a room with a random code', async () => { + const dto = { + name: 'Test Room Name', + description: 'Test Room Description', + }; + const result = await service.create(dto); + expect(result).toHaveProperty('id', '1'); + expect(result).toHaveProperty('name', dto.name); + expect(result).toHaveProperty('description', dto.description); + expect(result).toHaveProperty('code'); + expect(result.code).toMatch(/^[A-Z0-9]{6}$/); // Ensures random code format + expect(repository.create).toHaveBeenCalled(); + expect(repository.save).toHaveBeenCalled(); + }); + + it('should return all rooms', async () => { + await expect(service.findAll()).resolves.toEqual([]); + expect(repository.find).toHaveBeenCalled(); + }); + + it('should return a room by id', async () => { + await expect(service.findOne('1')).resolves.toEqual({ + id: '1', + name: 'Test Room Name', + description: 'Test Room Description', + code: 'RANDOM', + }); + expect(repository.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + }); + + it('should throw an error if room not found', async () => { + await expect(service.findOne('2')).rejects.toThrow(NotFoundException); + }); + + it('should update a room', async () => { + const updateDto = { + name: 'Updated Room', + description: 'Test Room Description', + }; + await expect(service.update('1', updateDto)).resolves.toEqual({ + id: '1', + ...updateDto, + code: 'RANDOM', + }); + expect(repository.save).toHaveBeenCalled(); + }); + + it('should remove a room', async () => { + await expect(service.remove('1')).resolves.toBeTruthy(); + expect(repository.remove).toHaveBeenCalled(); + }); +}); diff --git a/backend/src/room/room.service.ts b/backend/src/room/room.service.ts new file mode 100644 index 00000000..96c9549a --- /dev/null +++ b/backend/src/room/room.service.ts @@ -0,0 +1,49 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateRoomDto } from './dto/create-room.dto'; +import { UpdateRoomDto } from './dto/update-room.dto'; +import { Room } from './entities/room.entity'; + +@Injectable() +export class RoomService { + constructor( + @InjectRepository(Room) + private roomRepository: Repository, + ) {} + + private generateRoomCode(): string { + return Math.random().toString(36).substring(2, 8).toUpperCase(); + } + + create(createRoomDto: CreateRoomDto) { + const room = this.roomRepository.create({ + ...createRoomDto, + code: this.generateRoomCode(), + }); + return this.roomRepository.save(room); + } + + findAll() { + return this.roomRepository.find(); + } + + async findOne(id: string) { + const room = await this.roomRepository.findOne({ where: { id } }); + if (!room) { + throw new NotFoundException(`Room with ID ${id} not found`); + } + return room; + } + + async update(id: string, updateRoomDto: UpdateRoomDto) { + const room = await this.findOne(id); + Object.assign(room, updateRoomDto); + return this.roomRepository.save(room); + } + + async remove(id: string) { + const room = await this.findOne(id); + return this.roomRepository.remove(room); + } +}