Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/modules/event/dto/create-event.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IsString, IsNotEmpty, IsOptional, IsDateString, IsInt, Min } from 'class-validator';

export class CreateEventDto {
@IsString()
@IsNotEmpty()
title: string;

@IsString()
@IsOptional()
description?: string;

@IsString()
@IsNotEmpty()
location: string;

@IsDateString()
startDate: string;

@IsDateString()
endDate: string;

@IsInt()
@Min(0)
capacity: number;
}
4 changes: 4 additions & 0 deletions src/modules/event/dto/update-event.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';

export class UpdateEventDto extends PartialType(CreateEventDto) {}
60 changes: 60 additions & 0 deletions src/modules/event/event.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EventController } from './event.controller';
import { EventService } from './event.service';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { User } from '../../user/user.entity';

describe('EventController', () => {
let controller: EventController;
let service: EventService;
const organizer: User = { id: 'org-1', email: 'org@test.com', passwordHash: '', role: 'organizer', createdAt: new Date() };

const mockService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EventController],
providers: [
{ provide: EventService, useValue: mockService },
],
}).compile();
controller = module.get<EventController>(EventController);
service = module.get<EventService>(EventService);
jest.clearAllMocks();
});

it('should create event', async () => {
const dto: CreateEventDto = { title: 't', location: 'l', startDate: '2025-10-01', endDate: '2025-10-02', capacity: 10 } as any;
mockService.create.mockResolvedValue({ ...dto, organizer });
const req = { user: organizer };
const result = await controller.create(dto, req);
expect(result.organizer).toBe(organizer);
});

it('should list events', async () => {
mockService.findAll.mockResolvedValue([{ id: '1', organizer }]);
const req = { user: organizer };
const result = await controller.findAll(req);
expect(result[0].organizer).toBe(organizer);
});

it('should get event by id', async () => {
mockService.findOne.mockResolvedValue({ id: '1', organizer });
const req = { user: organizer };
const result = await controller.findOne('1', req);
expect(result.organizer).toBe(organizer);
});

it('should update event', async () => {
mockService.update.mockResolvedValue({ id: '1', organizer, title: 'new' });
const req = { user: organizer };
const result = await controller.update('1', { title: 'new' } as UpdateEventDto, req);
expect(result.title).toBe('new');
});
});
33 changes: 33 additions & 0 deletions src/modules/event/event.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Controller, Post, Get, Patch, Param, Body, Request, UseGuards } from '@nestjs/common';
import { EventService } from './event.service';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
import { User } from '../../user/user.entity';

@UseGuards(JwtAuthGuard)
@Controller('events')
export class EventController {
constructor(private readonly eventService: EventService) {}

@Post()
async create(@Body() createEventDto: CreateEventDto, @Request() req: any) {
// Organizer is the authenticated user
return this.eventService.create(createEventDto, req.user as User);
}

@Get()
async findAll(@Request() req: any) {
return this.eventService.findAll(req.user as User);
}

@Get(':id')
async findOne(@Param('id') id: string, @Request() req: any) {
return this.eventService.findOne(id, req.user as User);
}

@Patch(':id')
async update(@Param('id') id: string, @Body() updateEventDto: UpdateEventDto, @Request() req: any) {
return this.eventService.update(id, updateEventDto, req.user as User);
}
}
29 changes: 29 additions & 0 deletions src/modules/event/event.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from '../../user/user.entity';

@Entity()
export class Event {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
title: string;

@Column({ type: 'text', nullable: true })
description?: string;

@Column()
location: string;

@Column({ type: 'timestamp' })
startDate: Date;

@Column({ type: 'timestamp' })
endDate: Date;

@Column({ type: 'int' })
capacity: number;

@ManyToOne(() => User, user => user.id, { eager: true })
organizer: User;
}
13 changes: 13 additions & 0 deletions src/modules/event/event.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Event } from './event.entity';
import { EventService } from './event.service';
import { EventController } from './event.controller';

@Module({
imports: [TypeOrmModule.forFeature([Event])],
controllers: [EventController],
providers: [EventService],
exports: [EventService],
})
export class EventModule {}
87 changes: 87 additions & 0 deletions src/modules/event/event.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EventService } from './event.service';
import { Event } from './event.entity';
import { User } from '../../user/user.entity';
import { Repository } from 'typeorm';
import { ForbiddenException, NotFoundException } from '@nestjs/common';

describe('EventService', () => {
let service: EventService;
let repo: Repository<Event>;
const mockRepo = {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
};
const organizer: User = { id: 'org-1', email: 'org@test.com', passwordHash: '', role: 'organizer', createdAt: new Date() };

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EventService,
{ provide: getRepositoryToken(Event), useValue: mockRepo },
],
}).compile();
service = module.get<EventService>(EventService);
repo = module.get<Repository<Event>>(getRepositoryToken(Event));
jest.clearAllMocks();
});

it('should create event with valid data', async () => {
const dto = { title: 't', location: 'l', startDate: '2025-10-01', endDate: '2025-10-02', capacity: 10 };
mockRepo.create.mockReturnValue({ ...dto, organizer });
mockRepo.save.mockResolvedValue({ ...dto, organizer, id: '1' });
const result = await service.create(dto as any, organizer);
expect(result.organizer).toBe(organizer);
expect(mockRepo.save).toHaveBeenCalled();
});

it('should throw if startDate >= endDate', async () => {
const dto = { title: 't', location: 'l', startDate: '2025-10-02', endDate: '2025-10-01', capacity: 10 };
await expect(service.create(dto as any, organizer)).rejects.toThrow(ForbiddenException);
});

it('should throw if capacity < 0', async () => {
const dto = { title: 't', location: 'l', startDate: '2025-10-01', endDate: '2025-10-02', capacity: -1 };
await expect(service.create(dto as any, organizer)).rejects.toThrow(ForbiddenException);
});

it('should find all events for organizer', async () => {
mockRepo.find.mockResolvedValue([{ id: '1', organizer }]);
const result = await service.findAll(organizer);
expect(result[0].organizer).toBe(organizer);
});

it('should find one event for organizer', async () => {
mockRepo.findOne.mockResolvedValue({ id: '1', organizer });
const result = await service.findOne('1', organizer);
expect(result.organizer).toBe(organizer);
});

it('should throw NotFound if event not found', async () => {
mockRepo.findOne.mockResolvedValue(undefined);
await expect(service.findOne('1', organizer)).rejects.toThrow(NotFoundException);
});

it('should update event with valid data', async () => {
const event = { id: '1', organizer, startDate: new Date('2025-10-01'), endDate: new Date('2025-10-02'), capacity: 10 };
mockRepo.findOne.mockResolvedValue(event);
mockRepo.save.mockResolvedValue({ ...event, title: 'new' });
const result = await service.update('1', { title: 'new' }, organizer);
expect(result.title).toBe('new');
});

it('should throw if update has invalid dates', async () => {
const event = { id: '1', organizer, startDate: new Date('2025-10-01'), endDate: new Date('2025-10-02'), capacity: 10 };
mockRepo.findOne.mockResolvedValue(event);
await expect(service.update('1', { startDate: '2025-10-03', endDate: '2025-10-02' }, organizer)).rejects.toThrow(ForbiddenException);
});

it('should throw if update has negative capacity', async () => {
const event = { id: '1', organizer, startDate: new Date('2025-10-01'), endDate: new Date('2025-10-02'), capacity: 10 };
mockRepo.findOne.mockResolvedValue(event);
await expect(service.update('1', { capacity: -5 }, organizer)).rejects.toThrow(ForbiddenException);
});
});
50 changes: 50 additions & 0 deletions src/modules/event/event.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Event } from './event.entity';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
import { User } from '../../user/user.entity';

@Injectable()
export class EventService {
constructor(
@InjectRepository(Event)
private eventRepository: Repository<Event>,
) {}

async create(createEventDto: CreateEventDto, organizer: User): Promise<Event> {
if (new Date(createEventDto.startDate) >= new Date(createEventDto.endDate)) {
throw new ForbiddenException('Start date must be before end date');
}
if (createEventDto.capacity < 0) {
throw new ForbiddenException('Capacity must be non-negative');
}
const event = this.eventRepository.create({ ...createEventDto, organizer });
return this.eventRepository.save(event);
}

async findAll(organizer: User): Promise<Event[]> {
return this.eventRepository.find({ where: { organizer } });
}

async findOne(id: string, organizer: User): Promise<Event> {
const event = await this.eventRepository.findOne({ where: { id, organizer } });
if (!event) throw new NotFoundException('Event not found');
return event;
}

async update(id: string, updateEventDto: UpdateEventDto, organizer: User): Promise<Event> {
const event = await this.findOne(id, organizer);
if (updateEventDto.startDate && updateEventDto.endDate) {
if (new Date(updateEventDto.startDate) >= new Date(updateEventDto.endDate)) {
throw new ForbiddenException('Start date must be before end date');
}
}
if (updateEventDto.capacity !== undefined && updateEventDto.capacity < 0) {
throw new ForbiddenException('Capacity must be non-negative');
}
Object.assign(event, updateEventDto);
return this.eventRepository.save(event);
}
}