Skip to content
Open
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
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,21 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"eslint-plugin-import": "^2.31.0",
"json2csv": "^6.0.0-alpha.2",
"nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.13.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.20",
"uuid": "^10.0.0"
},
Expand Down Expand Up @@ -110,4 +112,4 @@
"npm run lint"
]
}
}
}
42 changes: 42 additions & 0 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,5 +244,47 @@ describe('AuthService', () => {
);
expect(sendMailMock).toHaveBeenCalled();
});

it('should send a recovery email when user is found', async () => {
const email = '[email protected]';
const user = new User();
user.id = '123';
user.email = email;

jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(user);
jest.spyOn(jwtService, 'signAsync').mockResolvedValueOnce('mocked-token');
sendMailMock.mockResolvedValueOnce(true);

const result = await service.recoverPassword(email);

expect(result).toEqual({ success: true });
expect(userRepository.findOneBy).toHaveBeenCalledWith({ email });
expect(jwtService.signAsync).toHaveBeenCalledWith(
{ sub: user.id },
{ expiresIn: '30m' },
);
expect(sendMailMock).toHaveBeenCalled();
});
});

describe('changePassword', () => {
it('should successfully change the password if user exists', async () => {
const userId = '123';
const newPassword = 'newSecurePassword';
const user = new User();
user.id = userId;
user.password = 'oldPassword';

jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(user);
jest.spyOn(bcrypt, 'hash').mockResolvedValueOnce('hashed-new-password');
jest.spyOn(userRepository, 'save').mockResolvedValueOnce(user);

const result = await service.changePassword(userId, newPassword);

expect(result).toEqual({ success: true });
expect(userRepository.findOneBy).toHaveBeenCalledWith({ id: userId });
expect(bcrypt.hash).toHaveBeenCalledWith(newPassword, expect.any(Number));
expect(userRepository.save).toHaveBeenCalledWith(user);
});
});
});
49 changes: 49 additions & 0 deletions src/export/export.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import { ExportService, ExportOptions } from './export.service';
import { Response } from 'express';

@Controller('export')
export class ExportController {
constructor(private readonly exportService: ExportService) {}

@Get()
async exportToCsv(
@Query('userIds') userIds: string | undefined,
@Query('bookIds') bookIds: string | undefined,
@Res() res: Response,
) {
try {
if (!userIds && !bookIds) {
console.log('Nenhum userId ou bookIds foi fornecido na query.');
return res.status(400).json({
message: 'Parâmetro "userIds" ou "bookIds" é obrigatório.',
});
}

const userIdsArray = userIds
? userIds.split(',').map((id) => id.trim())
: [];
const bookIdsArray = bookIds
? bookIds.split(',').map((id) => id.trim())
: [];

const options: ExportOptions = {
userIds: userIdsArray,
bookIds: bookIdsArray,
};

const csv = await this.exportService.generateCsv(options);

res.header('Content-Type', 'text/csv');
res.attachment('export.csv');
return res.send(csv);
} catch (error) {
console.log(`Erro ao gerar o CSV: ${error.message}`);
return res.status(500).json({
message: error.message,
});
}
}
}

export default ExportController;
27 changes: 27 additions & 0 deletions src/export/export.mockBooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';

export interface Book {
id: string;
titulo: string;
autor: string;
tema: string;
rating: number;
imageUrl: string;
}

@Injectable()
export class BooksService {
private books: Book[] = Array.from({ length: 120 }, (_, i) => ({
id: `${i + 1}`,
titulo: `Título ${Math.floor(i / 2) + 1}`,
autor: `Autor ${(i % 28) + 1}`,
tema: `Tema ${i + 1}`,
rating: parseFloat((Math.random() * 5).toFixed(2)),
imageUrl:
'https://plus.unsplash.com/premium_photo-1682125773446-259ce64f9dd7?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
}));

async findBooksByIds(bookIds: string[]): Promise<Book[]> {
return this.books.filter((book) => bookIds.includes(book.id));
}
}
12 changes: 12 additions & 0 deletions src/export/export.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ExportController } from './export.controller';
import { ExportService } from './export.service';
import { UsersModule } from '../users/users.module';
import { BooksService } from './export.mockBooks';

@Module({
imports: [UsersModule],
controllers: [ExportController],
providers: [ExportService, BooksService],
})
export class ExportModule {}
124 changes: 124 additions & 0 deletions src/export/export.service.spec.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigir lint e testes que estão falhando se for possivel

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os testes estão falhando por conta de timeout excedendo o limite padrão do Jest, possível solução:

  • aumentar o limite do timeout do Jest

Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExportService, ExportOptions } from './export.service';
import { UsersService } from '../users/users.service';
import { BooksService } from './export.mockBooks';
import { parse } from 'json2csv';

describe('ExportService', () => {
let exportService: ExportService;

const mockUsersService = {
findByIds: jest.fn(),
};

const mockBooksService = {
findBooksByIds: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ExportService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: BooksService, useValue: mockBooksService },
],
}).compile();

exportService = module.get<ExportService>(ExportService);
});

it('should be defined', () => {
expect(exportService).toBeDefined();
});

describe('generateCsv', () => {
it('should generate a valid CSV for users and books', async () => {
const mockUsers = [
{
id: '1',
firstName: 'Bruno',
lastName: 'Cruz',
email: '[email protected]',
phone: '123456789',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
const mockBooks = [
{
id: '101',
titulo: 'Titulo do livro',
autor: 'Author do livro',
tema: 'Tema do livro',
rating: 5,
imageUrl: 'image.jpg',
},
];

mockUsersService.findByIds.mockResolvedValue(mockUsers);
mockBooksService.findBooksByIds.mockResolvedValue(mockBooks);

const options: ExportOptions = { userIds: ['1'], bookIds: ['101'] };
const result = await exportService.generateCsv(options);

const expectedUserCsv = parse(
mockUsers.map((user) => ({
id: user.id,
name: `${user.firstName} ${user.lastName}`,
lastName: user.lastName,
email: user.email,
phone: user.phone,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
})),
{
fields: [
{ label: 'ID', value: 'id' },
{ label: 'Nome', value: 'name' },
{ label: 'Sobrenome', value: 'lastName' },
{ label: 'Email', value: 'email' },
{ label: 'Telefone', value: 'phone' },
{ label: 'Criado em', value: 'createdAt' },
{ label: 'Atualizado em', value: 'updatedAt' },
],
},
);

const expectedBookCsv = parse(mockBooks, {
fields: [
{ label: 'ID', value: 'id' },
{ label: 'Titulo', value: 'titulo' },
{ label: 'Autor', value: 'autor' },
{ label: 'Tema', value: 'tema' },
{ label: 'Avaliacao', value: 'rating' },
{ label: 'Capa', value: 'imageUrl' },
],
});

expect(result).toEqual(`${expectedUserCsv}\n${expectedBookCsv}`);
});

it('should throw an error if no userIds or bookIds are provided', async () => {
const options: ExportOptions = {};
await expect(exportService.generateCsv(options)).rejects.toThrowError(
'Nenhum usuário ou livro encontrado para exportação. Verifique os IDs fornecidos.',
);
});

it('should throw an error if some bookIds are not found', async () => {
mockBooksService.findBooksByIds.mockResolvedValue([]);
const options: ExportOptions = { bookIds: ['999'] };
await expect(exportService.generateCsv(options)).rejects.toThrowError(
'Os seguintes IDs de livros não foram encontrados no banco de dados: 999',
);
});

it('should throw an error if some userIds are not found', async () => {
mockBooksService.findBooksByIds.mockResolvedValue([]);
const options: ExportOptions = { userIds: ['888'] };
await expect(exportService.generateCsv(options)).rejects.toThrowError(
'Os seguintes IDs de usuários não foram encontrados no banco de dados: 888',
);
});
});
});
Loading
Loading