Skip to content

Commit

Permalink
Make it prettier, make it better?
Browse files Browse the repository at this point in the history
  • Loading branch information
SebCourvoisier committed Dec 4, 2024
1 parent e1a2747 commit 9483f64
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 167 deletions.
6 changes: 2 additions & 4 deletions src/api/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ import { BatchModule } from '../batch/batch.module'
configureLoggerModule(),
AuthModule,
FileModule,
BatchModule,
BatchModule
],
controllers: [RedirectController, HealthController, DecisionController],
providers: [
BucketHealthIndicator,
]
providers: [BucketHealthIndicator]
})
export class AppModule {}
4 changes: 1 addition & 3 deletions src/api/domain/decisions/repositories/decision.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ export interface DecisionRepository {
jsonS3Key: string
): Promise<void>

deleteDataDecisionIntegre(
jsonS3Key: string
): Promise<void>
deleteDataDecisionIntegre(jsonS3Key: string): Promise<void>

uploadFichierDecisionIntegre(
fichierDecisionIntegre: Express.Multer.File | Buffer,
Expand Down
17 changes: 10 additions & 7 deletions src/api/infrastructure/controllers/decision/decision.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ export class DecisionController {
description: "Une erreur inattendue liée à une dépendance de l'API a été rencontrée. "
})
@HttpCode(HttpStatus.NO_CONTENT)
async deleteDecision(@Param('decisionId') decisionId: string, @Req() request: Request) : Promise<DeleteDecisionResponse> {
async deleteDecision(
@Param('decisionId') decisionId: string,
@Req() request: Request
): Promise<DeleteDecisionResponse> {
const routePath = request.method + ' ' + request.path
const decisionUseCase = new DeleteDecisionUsecase(new DecisionS3Repository(this.logger))
const formatLogs: LogsFormat = {
Expand Down Expand Up @@ -152,29 +155,29 @@ export class DecisionController {
@ApiConsumes('multipart/form-data')
@ApiBody({
description:
'Les ressources pour modifier des décisions intègres sont :\n* PUT : envoi d\'une décision intègre',
"Les ressources pour modifier des décisions intègres sont :\n* PUT : envoi d'une décision intègre",
type: ReceiveDto
})
@ApiCreatedResponse({ description: 'La requête a été acceptée et va être traitée.' })
@ApiBadRequestResponse({
description: 'La requête n\'est pas correcte'
description: "La requête n'est pas correcte"
})
@ApiInternalServerErrorResponse({
description: 'Une erreur interne s\'est produite'
description: "Une erreur interne s'est produite"
})
@ApiUnauthorizedResponse({
description: 'La requête n\'est pas autorisée'
description: "La requête n'est pas autorisée"
})
@ApiServiceUnavailableResponse({
description: 'Une erreur inattendue liée à une dépendance de l\'API a été rencontrée. '
description: "Une erreur inattendue liée à une dépendance de l'API a été rencontrée. "
})
@HttpCode(HttpStatus.CREATED)
@UseInterceptors(FileInterceptor('fichierDecisionIntegre'))
async receiveDecision(
@UploadedFile() fichierDecisionIntegre: Express.Multer.File,
@Body('texteDecisionIntegre') texteDecisionIntegre: string,
@Body('metadonnees', new StringToJsonPipe(), new ValidateDtoPipe())
metadonneeDto: MetadonneeDto,
metadonneeDto: MetadonneeDto,
@Req() request: Request
): Promise<DecisionResponse> {
if (!fichierDecisionIntegre || !isPdfFile(fichierDecisionIntegre.mimetype)) {
Expand Down
2 changes: 1 addition & 1 deletion src/api/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ db.saveClient({

async function bootstrap() {
const appOptions = {
logger: ['log', 'error', 'warn'],
logger: ['log', 'error', 'warn']
} as NestApplicationOptions

const app = await NestFactory.create(AppModule, appOptions)
Expand Down
12 changes: 3 additions & 9 deletions src/api/usecase/deleteDecision.usecase.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { DecisionRepository } from '../domain/decisions/repositories/decision.repository'

export class DeleteDecisionUsecase {
constructor(private decisionsRepository: DecisionRepository) {}

constructor(private decisionsRepository: DecisionRepository) {
}

async deleteDecision(
decisionId: string
): Promise<string> {
async deleteDecision(decisionId: string): Promise<string> {
const jsonFileName = `${decisionId}.json`

await this.decisionsRepository.deleteDataDecisionIntegre(
jsonFileName
)
await this.decisionsRepository.deleteDataDecisionIntegre(jsonFileName)

return jsonFileName
}
Expand Down
4 changes: 1 addition & 3 deletions src/api/usecase/saveDecision.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DecisionRepository } from '../domain/decisions/repositories/decision.re
import { MetadonneeDto } from '../../shared/infrastructure/dto/metadonnee.dto'
import * as fs from 'fs'


const fakeFilename = 'test'
jest.mock('uuid', () => ({ v4: () => fakeFilename }))
jest.mock('fs', () => ({
Expand All @@ -32,8 +31,7 @@ describe('SaveDecision Usecase', () => {
path: ''
} as unknown as Express.Multer.File
const metadonnees = new MockUtils().metadonneeDtoMock as unknown as MetadonneeDto
(fs.writeFileSync as jest.Mock).mockImplementation(() => {
})
;(fs.writeFileSync as jest.Mock).mockImplementation(() => {})
await usecase.putDecision(fichierDecisionIntegre, 'test', metadonnees)

const requestDto = {
Expand Down
3 changes: 1 addition & 2 deletions src/api/usecase/saveDecision.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { CollectDto } from 'src/shared/infrastructure/dto/collect.dto'
export class SaveDecisionUsecase {
private readonly fileService: FileService = new FileService()

constructor(private decisionsRepository: DecisionRepository) {
}
constructor(private decisionsRepository: DecisionRepository) {}

async putDecision(
fichierDecisionIntegre: Express.Multer.File,
Expand Down
7 changes: 2 additions & 5 deletions src/batch/batch.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import { BatchService } from './batch.service'
import { ScheduleModule } from '@nestjs/schedule'

@Module({
imports: [
ScheduleModule.forRoot()
],
imports: [ScheduleModule.forRoot()],
providers: [BatchService],
exports: [BatchService]
})
export class BatchModule {
}
export class BatchModule {}
62 changes: 34 additions & 28 deletions src/batch/batch.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ describe('BatchService', () => {
let schedulerRegistry: jest.Mocked<SchedulerRegistry>

const mockFolderPath = '/mock-folder'
const mockFileNames = ['05526bff-58be-4f35-b0a9-b3bfbdd9e1ea_file_1.pdf',
'c5e1783b-9fa4-4147-9b16-90b902060866_file2.pdf']
const mockFileNames = [
'05526bff-58be-4f35-b0a9-b3bfbdd9e1ea_file_1.pdf',
'c5e1783b-9fa4-4147-9b16-90b902060866_file2.pdf'
]
const mockFileData = Buffer.from('mock file data')

beforeEach(() => {
Expand All @@ -47,20 +49,19 @@ describe('BatchService', () => {
addCronJob: jest.fn()
} as unknown as jest.Mocked<SchedulerRegistry>

batchService = new BatchService(schedulerRegistry)
;(batchService as any).decisionsRepository = decisionRepository
;(batchService as any).logger = logger

batchService = new BatchService(schedulerRegistry);
(batchService as any).decisionsRepository = decisionRepository;
(batchService as any).logger = logger;

(fs.readdirSync as jest.Mock).mockReturnValue(mockFileNames)
;(fs.readdirSync as jest.Mock).mockReturnValue(mockFileNames)
})

afterEach(() => {
jest.clearAllMocks()
})

it('should log an error if reading files fails', async () => {
(fs.readdirSync as jest.Mock).mockImplementation(() => {
;(fs.readdirSync as jest.Mock).mockImplementation(() => {
throw new Error('File read error')
})

Expand All @@ -74,48 +75,54 @@ describe('BatchService', () => {
})

it('should process files in the folder and upload them', async () => {
(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`);
(fs.statSync as jest.Mock).mockReturnValue({ isFile: () => true });
(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData);
(fs.unlinkSync as jest.Mock).mockImplementation(() => {
})
;(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`)
;(fs.statSync as jest.Mock).mockReturnValue({ isFile: () => true })
;(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData)
;(fs.unlinkSync as jest.Mock).mockImplementation(() => {})

await batchService.archiveFilesToS3()

mockFileNames.forEach((filename) => {
expect(decisionRepository.uploadFichierDecisionIntegre).toHaveBeenCalledTimes(mockFileNames.length)
expect(decisionRepository.uploadFichierDecisionIntegre).toHaveBeenCalledTimes(
mockFileNames.length
)
expect(fs.unlinkSync).toHaveBeenCalledWith(`${mockFolderPath}/${filename}`)
})

expect(logger.log).toHaveBeenCalledWith({ msg: 'Starting scan', operationName: 'archiveFilesToS3' })
expect(logger.log).toHaveBeenCalledWith({ msg: 'End of scan', operationName: 'archiveFilesToS3' })
expect(logger.log).toHaveBeenCalledWith({
msg: 'Starting scan',
operationName: 'archiveFilesToS3'
})
expect(logger.log).toHaveBeenCalledWith({
msg: 'End of scan',
operationName: 'archiveFilesToS3'
})
})

it('should skip non-file entries in the folder', async () => {
const mockEntries = ['file1.pdf', 'file2.pdf'];
const mockEntries = ['file1.pdf', 'file2.pdf']

(fs.readdirSync as jest.Mock).mockReturnValue(mockEntries);
(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`);
(fs.statSync as jest.Mock).mockImplementation(() => {
;(fs.readdirSync as jest.Mock).mockReturnValue(mockEntries)
;(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`)
;(fs.statSync as jest.Mock).mockImplementation(() => {
return {
isFile: () => true
}
});
(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData);
(fs.unlinkSync as jest.Mock).mockImplementation(() => {
})
;(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData)
;(fs.unlinkSync as jest.Mock).mockImplementation(() => {})

await batchService.archiveFilesToS3()

expect(fs.unlinkSync).toHaveBeenCalledWith(`${mockFolderPath}/file1.pdf`)
})

it('should throw and log an error if upload fails', async () => {
(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`);
(fs.statSync as jest.Mock).mockReturnValue({ isFile: () => true });
(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData);
;(path.join as jest.Mock).mockImplementation((folder, file) => `${folder}/${file}`)
;(fs.statSync as jest.Mock).mockReturnValue({ isFile: () => true })
;(fs.readFileSync as jest.Mock).mockReturnValue(mockFileData)

(decisionRepository.uploadFichierDecisionIntegre as jest.Mock).mockImplementation(() => {
;(decisionRepository.uploadFichierDecisionIntegre as jest.Mock).mockImplementation(() => {
throw new Error('Upload error')
})

Expand All @@ -126,6 +133,5 @@ describe('BatchService', () => {
msg: 'Upload error',
operationName: 'archiveFilesToS3'
})

})
})
18 changes: 10 additions & 8 deletions src/batch/batch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ import { DecisionRepository } from '../api/domain/decisions/repositories/decisio
import { DecisionS3Repository } from '../shared/infrastructure/repositories/decisionS3.repository'
import { CronJob } from 'cron'


@Injectable()
export class BatchService implements OnModuleInit {
private readonly folderPath = process.env.AV_PDF_PATH
private readonly separator = process.env.S3_PDF_FILE_NAME_SEPARATOR
private readonly logger: Logger = new Logger(BatchService.name)
private readonly decisionsRepository: DecisionRepository = new DecisionS3Repository(this.logger)

constructor(private schedulerRegistry: SchedulerRegistry) {
}
constructor(private schedulerRegistry: SchedulerRegistry) {}

onModuleInit() {
this.addCronJob(`archive_files_to_s3`, process.env.S3_ARCHIVE_SCHEDULE, this.archiveFilesToS3.bind(this))
this.addCronJob(
`archive_files_to_s3`,
process.env.S3_ARCHIVE_SCHEDULE,
this.archiveFilesToS3.bind(this)
)
}

async archiveFilesToS3() {
this.logger.log({ operationName: 'archiveFilesToS3', msg: `Starting scan` })

try {
const fileNames = fs.readdirSync(this.folderPath)
fileNames.forEach(filename => {
fileNames.forEach((filename) => {
const filePath = path.join(this.folderPath, filename)
const stats = fs.statSync(filePath)

Expand All @@ -47,7 +49,6 @@ export class BatchService implements OnModuleInit {
const filePath = path.join(this.folderPath, filename)
fs.unlinkSync(filePath) // file deletion
})

} catch (error) {
this.logger.error({ operationName: 'archiveFilesToS3', msg: error.message, data: error })
}
Expand All @@ -70,7 +71,8 @@ export class BatchService implements OnModuleInit {
this.schedulerRegistry.addCronJob(name, job)
job.start()

this.logger.log(`The cron job ${name} has been added with the following cron expression : ${cronExpression}`)
this.logger.log(
`The cron job ${name} has been added with the following cron expression : ${cronExpression}`
)
}

}
20 changes: 8 additions & 12 deletions src/shared/infrastructure/files/file.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,16 @@ describe('FileService', () => {
})

it('should create the upload directory if it does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
(fs.mkdirSync as jest.Mock).mockImplementation(() => {
})
;(fs.existsSync as jest.Mock).mockReturnValue(false)
;(fs.mkdirSync as jest.Mock).mockImplementation(() => {})

new FileService()

expect(fs.existsSync).toHaveBeenCalledWith(mockUploadPath)
})

it('should not create the upload directory if it already exists', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true)
;(fs.existsSync as jest.Mock).mockReturnValue(true)

new FileService()

Expand All @@ -41,9 +40,8 @@ describe('FileService', () => {

it('should save the file successfully and return the file path and name', () => {
const mockFilename = 'uniqueFile.pdf'
const mockFullPath = `${mockUploadPath}/${mockFilename}`;
(fs.writeFileSync as jest.Mock).mockImplementation(() => {
})
const mockFullPath = `${mockUploadPath}/${mockFilename}`
;(fs.writeFileSync as jest.Mock).mockImplementation(() => {})

const result = fileService.saveFile(mockFile, mockFilename)

Expand All @@ -55,13 +53,11 @@ describe('FileService', () => {
})

it('should throw an InternalServerErrorException if saving the file fails', () => {
const mockFilename = 'uniqueFile.pdf';
(fs.writeFileSync as jest.Mock).mockImplementation(() => {
const mockFilename = 'uniqueFile.pdf'
;(fs.writeFileSync as jest.Mock).mockImplementation(() => {
throw new Error('Write failed')
})

expect(() => fileService.saveFile(mockFile, mockFilename)).toThrow(
InternalServerErrorException
)
expect(() => fileService.saveFile(mockFile, mockFilename)).toThrow(InternalServerErrorException)
})
})
8 changes: 2 additions & 6 deletions src/shared/infrastructure/files/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ export class FileService {
path: fullPath
}
} catch (error) {
throw new InternalServerErrorException(
'Error saving file'
)
throw new InternalServerErrorException('Error saving file')
}
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ export class DecisionS3Repository implements DecisionRepository {
}
}

async deleteDataDecisionIntegre(
jsonS3Key: string
): Promise<void> {
async deleteDataDecisionIntegre(jsonS3Key: string): Promise<void> {
const reqParamsMarkForDeletion = {
Body: JSON.stringify({
date: new Date()
Expand Down
Loading

0 comments on commit 9483f64

Please sign in to comment.