Compare commits

1 Commits

Author SHA1 Message Date
b48e59b073 fix 2025-09-28 00:40:38 +07:00
33 changed files with 2857 additions and 499 deletions

2344
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,12 +21,18 @@
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"@types/pg": "^8.15.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"typeorm": "^0.3.27"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",

View File

@@ -1,10 +1,33 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { AuthenticationModule } from './authentication/authentication.module'; import { AuthenticationModule } from './authentication/authentication.module';
import { UsersModule } from './users/users.module';
@Module({ @Module({
imports: [AuthenticationModule], imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
url: configService.get('DATABASE_URL') || 'postgresql://postgres.zgmimnyowgdnlwccjubz:renolation29@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
ssl: {
rejectUnauthorized: false,
},
}),
inject: [ConfigService],
}),
AuthenticationModule,
UsersModule,
],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })

View File

@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthenticationController } from './authentication.controller'; import { AuthenticationController } from './authentication.controller';
import { AuthenticationService } from './authentication.service'; import { AuthenticationService } from './authentication.service';
import { UsersModule } from '../users/users.module';
/** /**
* Authentication module for handling user authentication * Authentication module for handling user authentication
*/ */
@Module({ @Module({
imports: [UsersModule],
controllers: [AuthenticationController], controllers: [AuthenticationController],
providers: [AuthenticationService], providers: [AuthenticationService],
exports: [AuthenticationService], exports: [AuthenticationService],

View File

@@ -24,22 +24,19 @@ describe('AuthenticationService', () => {
email: 'test@example.com', email: 'test@example.com',
password: 'password123', password: 'password123',
}; };
const expectedResponse = {
message: 'Login successful',
user: {
email: 'test@example.com',
},
token: 'placeholder-token',
};
const actualResponse = await service.login(inputLoginDto); const actualResponse = await service.login(inputLoginDto);
expect(actualResponse).toEqual(expectedResponse); expect(actualResponse).toHaveProperty('message', 'Login successful');
expect(actualResponse).toHaveProperty('user');
expect(actualResponse.user).toHaveProperty('email', 'test@example.com');
expect(actualResponse).toHaveProperty('token');
expect(actualResponse.token).toMatch(/^auth_token_\d+_[a-z0-9]+$/);
}); });
it('should throw UnauthorizedException with invalid email', async () => { it('should throw UnauthorizedException with empty email', async () => {
const inputLoginDto: LoginDto = { const inputLoginDto: LoginDto = {
email: 'invalid@example.com', email: '',
password: 'password123', password: 'password123',
}; };
@@ -48,10 +45,10 @@ describe('AuthenticationService', () => {
); );
}); });
it('should throw UnauthorizedException with invalid password', async () => { it('should throw UnauthorizedException with empty password', async () => {
const inputLoginDto: LoginDto = { const inputLoginDto: LoginDto = {
email: 'test@example.com', email: 'test@example.com',
password: 'wrongpassword', password: '',
}; };
await expect(service.login(inputLoginDto)).rejects.toThrow( await expect(service.login(inputLoginDto)).rejects.toThrow(

View File

@@ -1,5 +1,6 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { UserService } from '../users/user.service';
/** /**
* Response interface for successful login * Response interface for successful login
@@ -7,9 +8,11 @@ import { LoginDto } from './dto/login.dto';
export interface LoginResponse { export interface LoginResponse {
message: string; message: string;
user: { user: {
id: string;
email: string; email: string;
name: string;
}; };
token?: string; token: string;
} }
/** /**
@@ -17,6 +20,8 @@ export interface LoginResponse {
*/ */
@Injectable() @Injectable()
export class AuthenticationService { export class AuthenticationService {
constructor(private readonly userService: UserService) {}
/** /**
* Authenticates a user with email and password * Authenticates a user with email and password
* @param loginDto - Login credentials containing email and password * @param loginDto - Login credentials containing email and password
@@ -26,33 +31,33 @@ export class AuthenticationService {
async login(loginDto: LoginDto): Promise<LoginResponse> { async login(loginDto: LoginDto): Promise<LoginResponse> {
const { email, password } = loginDto; const { email, password } = loginDto;
// TODO: Implement actual user validation logic if (!email || !password) {
// This is a placeholder implementation throw new UnauthorizedException('Email and password are required');
const isValidUser = await this.validateUser(email, password); }
if (!isValidUser) { // Find user by email
const user = await this.userService.findUserByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid email or password'); throw new UnauthorizedException('Invalid email or password');
} }
// Simple password validation (in production, use bcrypt)
if (user.password !== password) {
throw new UnauthorizedException('Invalid email or password');
}
// Generate a simple token (in production, use JWT)
const token = `auth_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return { return {
message: 'Login successful', message: 'Login successful',
user: { user: {
email, id: user.id,
email: user.email,
name: user.name,
}, },
// TODO: Generate JWT token token,
token: 'placeholder-token',
}; };
} }
/**
* Validates user credentials
* @param email - User email
* @param password - User password
* @returns Promise<boolean> - Whether credentials are valid
*/
private async validateUser(email: string, password: string): Promise<boolean> {
// TODO: Implement actual user validation against database
// For now, using placeholder validation
return email === 'test@example.com' && password === 'password123';
}
} }

View File

@@ -1,30 +0,0 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import {UsersService} from "../users/user.service";
@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh-token'
) {
constructor(
private readonly configService: ConfigService,
private readonly userService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
return request?.cookies?.Refresh;
}]),
secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'),
passReqToCallback: true,
});
}
async validate(request: Request, payload: TokenPayload) {
const refreshToken = request.cookies?.Refresh;
return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.userId);
}
}

View File

@@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}

View File

@@ -1,44 +0,0 @@
import {
ConnectedSocket,
MessageBody, OnGatewayConnection,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import {Server, Socket} from 'socket.io';
import {ChatService} from "./chat.service";
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;
constructor(
private readonly chatService: ChatService
) {
}
async handleConnection(socket: Socket) {
await this.chatService.getUserFromSocket(socket);
}
@SubscribeMessage('send_message')
async listenForMessages(@MessageBody() content: string, @ConnectedSocket() socket: Socket,) {
const author = await this.chatService.getUserFromSocket(socket);
const message = await this.chatService.saveMessage(content, author);
this.server.sockets.emit('receive_message', message);
return message;
}
@SubscribeMessage('request_all_messages')
async requestAllMessages(
@ConnectedSocket() socket: Socket,
) {
await this.chatService.getUserFromSocket(socket);
const messages = await this.chatService.getAllMessages();
socket.emit('send_all_messages', messages);
}
}

View File

@@ -1,47 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthenticationService } from '../authentication/authentication.service';
import { Socket } from 'socket.io';
import { parse } from 'cookie';
import { WsException } from '@nestjs/websockets';
import UserEntity from "../users/entities/user.entity";
import {InjectRepository} from "@nestjs/typeorm";
import Message from "./mesage.entity";
import {Repository} from "typeorm";
@Injectable()
export class ChatService {
constructor(
private readonly authenticationService: AuthenticationService,
@InjectRepository(Message)
private messagesRepository: Repository<Message>,
) {
}
async saveMessage(content: string, author: UserEntity) {
const newMessage = this.messagesRepository.create({
content,
author
});
await this.messagesRepository.save(newMessage);
return newMessage;
}
async getAllMessages() {
return this.messagesRepository.find({
relations: {
author: true,
}
});
}
async getUserFromSocket(socket: Socket) {
const cookie = socket.handshake.headers.cookie;
const { Authentication: authenticationToken } = parse(cookie);
const user = await this.authenticationService.getUserFromAuthenticationToken(authenticationToken);
if (!user) {
throw new WsException('Invalid credentials.');
}
return user;
}
}

View File

@@ -1,16 +0,0 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import User from '../users/entities/user.entity';
@Entity()
class Message {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public content: string;
@ManyToOne(() => User)
public author: User;
}
export default Message;

View File

@@ -3,6 +3,6 @@ import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000); await app.listen(4003);
} }
bootstrap(); bootstrap();

View File

@@ -1,19 +0,0 @@
import { CACHE_KEY_METADATA, CacheInterceptor, } from '@nestjs/cache-manager';
import {ExecutionContext, Injectable} from "@nestjs/common";
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
const cacheKey = this.reflector.get(
CACHE_KEY_METADATA,
context.getHandler(),
);
if (cacheKey) {
const request = context.switchToHttp().getRequest();
return `${cacheKey}-${request._parsedUrl.query}`;
}
return super.trackBy(context);
}
}

View File

@@ -1 +0,0 @@
export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE';

View File

@@ -1,98 +0,0 @@
import {Injectable} from '@nestjs/common';
import {ElasticsearchService} from '@nestjs/elasticsearch';
import Post from "./entities/post.entity";
import {PostSearchResult} from "./types/postSearchResult.interface";
import {PostSearchBody} from "./types/postSearchBody.interface";
import PostCountResult from "./types/postCountBody.interface";
@Injectable()
export default class PostsSearchService {
index = 'posts';
constructor(
private readonly elasticsearchService: ElasticsearchService
) {
}
async indexPost(post: Post) {
return this.elasticsearchService.index<PostSearchBody>({
index: this.index,
document: {
id: post.id,
title: post.title,
content: post.content,
authorId: post.author.id
}
});
}
async count(query: string, fields: string[]) {
const result = await this.elasticsearchService.count({
index: this.index,
query: {
multi_match: {
query,
fields,
},
},
});
return result.count;
}
async search(text: string) {
const result = await this.elasticsearchService.search<PostSearchResult>({
index: this.index,
query: {
multi_match: {
query: text,
fields: ['title', 'content'],
},
},
});
const hits = result.hits.hits;
return hits.map((item) => item._source);
}
async remove(postId: number) {
await this.elasticsearchService.deleteByQuery({
index: this.index,
query: {
match: {
id: postId,
}
}
})
}
async update(post: Post) {
const newBody: PostSearchBody = {
id: post.id,
title: post.title,
content: post.content,
authorId: post.author.id
};
const script = Object.entries(newBody).reduce((result, [key, value]) => {
return `${result} ctx._source.${key}='${value}';`;
}, '');
await this.elasticsearchService.updateByQuery({
index: this.index,
query: {
match: {
id: post.id,
}
},
script: {
source: script
}
});
}
}

View File

@@ -1,5 +0,0 @@
interface PostCountResult {
count: number;
}
export default PostCountResult;

View File

@@ -1,6 +0,0 @@
export interface PostSearchBody {
id: number,
title: string,
content: string,
authorId: number
}

View File

@@ -1,10 +0,0 @@
import {PostSearchBody} from "./postSearchBody.interface";
export interface PostSearchResult {
hits: {
total: number;
hits: Array<{
_source: PostSearchBody;
}>;
};
}

View File

@@ -1,19 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import Product from '../products/product.entity';
@Entity()
class ProductCategory {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public name: string;
@OneToMany(
() => Product,
(product: Product) => product.category,
)
public products: Product[];
}
export default ProductCategory;

View File

@@ -1,23 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import ProductCategory from '../productCategories/productCategory.entity';
import { CarProperties } from './types/carProperties.interface';
import { BookProperties } from './types/bookProperties.interface';
@Entity()
class Product {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public name: string;
@ManyToOne(() => ProductCategory, (category: ProductCategory) => category.products)
public category: ProductCategory;
@Column({
type: 'jsonb'
})
public properties: CarProperties | BookProperties;
}
export default Product;

View File

@@ -1,4 +0,0 @@
export interface BookProperties {
authors: string[];
publicationYear: string;
}

View File

@@ -1,7 +0,0 @@
export interface CarProperties {
brand: string;
engine: {
fuel: string;
numberOfCylinders: number;
}
}

View File

@@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ElasticsearchModule } from '@nestjs/elasticsearch';
@Module({
imports: [
ConfigModule,
ElasticsearchModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
node: configService.get<string>('ELASTICSEARCH_NODE'),
auth: {
username: configService.get<string>('ELASTICSEARCH_USERNAME'),
password: configService.get<string>('ELASTICSEARCH_PASSWORD'),
}
}),
inject: [ConfigService],
}),
],
exports: [ElasticsearchModule]
})
export class SearchModule {}

View File

@@ -0,0 +1,20 @@
import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUrl, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(6, { message: 'Password must be at least 6 characters long' })
password: string;
@IsString()
@IsNotEmpty()
name: string;
@IsOptional()
@IsUrl()
avatarUrl?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}

31
src/users/user.entity.ts Normal file
View File

@@ -0,0 +1,31 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column()
name: string;
@Column({ nullable: true })
avatarUrl?: string;
@Column()
token: string;
@Column({ type: 'timestamp', nullable: true })
tokenExpiry?: Date;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,133 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';
describe('UserService', () => {
let service: UserService;
let repository: Repository<User>;
const mockRepository = {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UserService>(UserService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('createUser', () => {
it('should create a user with token and tokenExpiry', async () => {
const inputCreateUserDto: CreateUserDto = {
email: 'test@example.com',
name: 'Test User',
avatarUrl: 'https://example.com/avatar.jpg',
};
const expectedUser = {
...inputCreateUserDto,
id: 'uuid',
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.create.mockReturnValue(expectedUser);
mockRepository.save.mockResolvedValue(expectedUser);
const actualUser = await service.createUser(inputCreateUserDto);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
email: inputCreateUserDto.email,
name: inputCreateUserDto.name,
avatarUrl: inputCreateUserDto.avatarUrl,
token: expect.stringMatching(/^mock_jwt_token_\d+$/),
tokenExpiry: expect.any(Date),
}),
);
expect(mockRepository.save).toHaveBeenCalledWith(expectedUser);
expect(actualUser).toEqual(expectedUser);
});
});
describe('findUserById', () => {
it('should return a user when found', async () => {
const inputId = 'test-id';
const expectedUser = {
id: inputId,
email: 'test@example.com',
name: 'Test User',
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.findOne.mockResolvedValue(expectedUser);
const actualUser = await service.findUserById(inputId);
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: inputId } });
expect(actualUser).toEqual(expectedUser);
});
it('should throw NotFoundException when user not found', async () => {
const inputId = 'non-existent-id';
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findUserById(inputId)).rejects.toThrow(NotFoundException);
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: inputId } });
});
});
describe('refreshToken', () => {
it('should refresh user token and tokenExpiry', async () => {
const inputId = 'test-id';
const mockUser = {
id: inputId,
email: 'test@example.com',
name: 'Test User',
token: 'old_token',
tokenExpiry: new Date(),
save: jest.fn(),
};
mockRepository.findOne.mockResolvedValue(mockUser);
mockRepository.save.mockResolvedValue(mockUser);
const actualUser = await service.refreshToken(inputId);
expect(mockUser.token).toMatch(/^mock_jwt_token_\d+$/);
expect(mockUser.tokenExpiry).toBeInstanceOf(Date);
expect(mockRepository.save).toHaveBeenCalledWith(mockUser);
expect(actualUser).toEqual(mockUser);
});
});
});

75
src/users/user.service.ts Normal file
View File

@@ -0,0 +1,75 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async createUser(createUserDto: CreateUserDto): Promise<User> {
const token = `mock_jwt_token_${Date.now()}`;
const tokenExpiry = new Date();
tokenExpiry.setDate(tokenExpiry.getDate() + 365);
const user = this.userRepository.create({
...createUserDto,
token,
tokenExpiry,
});
return this.userRepository.save(user);
}
async findAllUsers(): Promise<User[]> {
return this.userRepository.find();
}
async findUserById(id: string): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async findUserByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({ where: { email } });
}
async updateUser(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findUserById(id);
Object.assign(user, updateUserDto);
return this.userRepository.save(user);
}
async deleteUser(id: string): Promise<void> {
const user = await this.findUserById(id);
await this.userRepository.remove(user);
}
async refreshToken(id: string): Promise<User> {
const user = await this.findUserById(id);
const token = `mock_jwt_token_${Date.now()}`;
const tokenExpiry = new Date();
tokenExpiry.setDate(tokenExpiry.getDate() + 365);
user.token = token;
user.tokenExpiry = tokenExpiry;
return this.userRepository.save(user);
}
async getUserIfRefreshTokenMatches(refreshToken: string, userId: string): Promise<User | null> {
const user = await this.findUserById(userId);
if (user && user.token === refreshToken) {
return user;
}
return null;
}
}

View File

@@ -0,0 +1,192 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
describe('UsersController', () => {
let controller: UsersController;
let service: UserService;
const mockUserService = {
createUser: jest.fn(),
findAllUsers: jest.fn(),
findUserById: jest.fn(),
updateUser: jest.fn(),
deleteUser: jest.fn(),
refreshToken: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UserService,
useValue: mockUserService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UserService>(UserService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('createUser', () => {
it('should create a user', async () => {
const inputCreateUserDto: CreateUserDto = {
email: 'test@example.com',
name: 'Test User',
avatarUrl: 'https://example.com/avatar.jpg',
};
const expectedUser = {
id: 'uuid',
...inputCreateUserDto,
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockUserService.createUser.mockResolvedValue(expectedUser);
const actualUser = await controller.createUser(inputCreateUserDto);
expect(mockUserService.createUser).toHaveBeenCalledWith(inputCreateUserDto);
expect(actualUser).toEqual(expectedUser);
});
});
describe('findAllUsers', () => {
it('should return all users', async () => {
const expectedUsers = [
{
id: 'uuid1',
email: 'test1@example.com',
name: 'Test User 1',
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: 'uuid2',
email: 'test2@example.com',
name: 'Test User 2',
token: 'mock_jwt_token_1234567891',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
},
];
mockUserService.findAllUsers.mockResolvedValue(expectedUsers);
const actualUsers = await controller.findAllUsers();
expect(mockUserService.findAllUsers).toHaveBeenCalled();
expect(actualUsers).toEqual(expectedUsers);
});
});
describe('findUserById', () => {
it('should return a user by id', async () => {
const inputId = 'test-id';
const expectedUser = {
id: inputId,
email: 'test@example.com',
name: 'Test User',
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockUserService.findUserById.mockResolvedValue(expectedUser);
const actualUser = await controller.findUserById(inputId);
expect(mockUserService.findUserById).toHaveBeenCalledWith(inputId);
expect(actualUser).toEqual(expectedUser);
});
});
describe('updateUser', () => {
it('should update a user', async () => {
const inputId = 'test-id';
const inputUpdateUserDto: UpdateUserDto = {
name: 'Updated Name',
};
const expectedUser = {
id: inputId,
email: 'test@example.com',
name: 'Updated Name',
token: 'mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockUserService.updateUser.mockResolvedValue(expectedUser);
const actualUser = await controller.updateUser(inputId, inputUpdateUserDto);
expect(mockUserService.updateUser).toHaveBeenCalledWith(inputId, inputUpdateUserDto);
expect(actualUser).toEqual(expectedUser);
});
});
describe('deleteUser', () => {
it('should delete a user', async () => {
const inputId = 'test-id';
mockUserService.deleteUser.mockResolvedValue(undefined);
await controller.deleteUser(inputId);
expect(mockUserService.deleteUser).toHaveBeenCalledWith(inputId);
});
});
describe('refreshToken', () => {
it('should refresh user token', async () => {
const inputId = 'test-id';
const expectedUser = {
id: inputId,
email: 'test@example.com',
name: 'Test User',
token: 'new_mock_jwt_token_1234567890',
tokenExpiry: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
mockUserService.refreshToken.mockResolvedValue(expectedUser);
const actualUser = await controller.refreshToken(inputId);
expect(mockUserService.refreshToken).toHaveBeenCalledWith(inputId);
expect(actualUser).toEqual(expectedUser);
});
});
describe('testEndpoint', () => {
it('should return test message', async () => {
const expectedMessage = { message: 'User module is working correctly' };
const actualMessage = await controller.testEndpoint();
expect(actualMessage).toEqual(expectedMessage);
});
});
});

View File

@@ -0,0 +1,57 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async createUser(@Body() createUserDto: CreateUserDto) {
console.log('createUserDto', createUserDto);
return this.userService.createUser(createUserDto);
}
@Get()
async findAllUsers() {
return this.userService.findAllUsers();
}
@Get(':id')
async findUserById(@Param('id') id: string) {
return this.userService.findUserById(id);
}
@Patch(':id')
async updateUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.updateUser(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteUser(@Param('id') id: string) {
await this.userService.deleteUser(id);
}
@Post(':id/refresh-token')
async refreshToken(@Param('id') id: string) {
return this.userService.refreshToken(id);
}
@Get('admin/test')
async testEndpoint() {
return { message: 'User module is working correctly' };
}
}

13
src/users/users.module.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserService } from './user.service';
import { UsersController } from './users.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UserService],
exports: [UserService],
})
export class UsersModule {}

View File

@@ -1,23 +0,0 @@
import { IsNumber, Min, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationParams {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
startId?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
offset?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number;
}

View File

@@ -1,11 +1,10 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import * as request from 'supertest'; import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module'; import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => { describe('AppController (e2e)', () => {
let app: INestApplication<App>; let app: INestApplication;
beforeEach(async () => { beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({ const moduleFixture: TestingModule = await Test.createTestingModule({