Skip to content

Commit

Permalink
fix: added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
shiv committed Jul 5, 2024
1 parent 57cf6bf commit f70e993
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 109 deletions.
104 changes: 43 additions & 61 deletions packages/common/src/services/file-upload.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,77 +10,68 @@ import { ConfigService } from '@nestjs/config';
@Injectable()
export class FileUploadService {
private readonly storage: any;
private readonly useService: boolean = this.configService.get<string>('STORAGE_MODE')?.toLowerCase() === this.configService.get<string>('STORAGE_MODE.MINIO');
private readonly useService: boolean;
private readonly fastifyInstance: FastifyInstance;
private readonly useSSL: boolean;
private readonly storageEndpoint: string;
private readonly storagePort: number;
private readonly bucketName: string;
private logger: Logger;

constructor(private readonly configService: ConfigService) {
this.logger = new Logger('FileUploadService');
this.useService = this.configService.get<string>('STORAGE_MODE')?.toLowerCase() === STORAGE_MODE.MINIO;
this.useSSL = this.configService.get<string>('STORAGE_USE_SSL') === 'true';
this.storageEndpoint = this.configService.get<string>('STORAGE_ENDPOINT');
this.storagePort = parseInt(this.configService.get('STORAGE_PORT'), 10);
this.bucketName = this.configService.get<string>('MINIO_BUCKETNAME');

switch (this.configService.get<string>('STORAGE_MODE')?.toLowerCase()) {
case this.configService.get<string>('STORAGE_MODE.MINIO'):
this.storage = new Client({
endPoint: this.configService.get<string>('STORAGE_ENDPOINT'),
port: parseInt(this.configService.get('STORAGE_PORT')),
useSSL:
this.configService.get<string>('CLIENT_USE_SSL').toLocaleLowerCase() === 'true'
? true
: false,
accessKey: this.configService.get('STORAGE_ACCESS_KEY'),
secretKey: this.configService.get('STORAGE_SECRET_KEY'),
});
break;

default:
this.fastifyInstance = fastify();
if (this.useService) {
this.storage = new Client({
endPoint: this.storageEndpoint,
port: this.storagePort,
useSSL: this.useSSL,
accessKey: this.configService.get('STORAGE_ACCESS_KEY'),
secretKey: this.configService.get('STORAGE_SECRET_KEY'),
});
} else {
this.fastifyInstance = fastify();
}
}



async uploadToMinio(filename: string, file: any): Promise<string> {
const metaData = {
'Content-Type': file.mimetype,
};
return new Promise((resolve, reject) => {
this.storage.putObject(
this.configService.get<string>('MINIO_BUCKETNAME'), //
this.bucketName,
filename,
file.buffer,
metaData,
function (err) {
(err) => {
if (err) {
console.log('err: ', err);
this.logger.error(`Error uploading to Minio: ${err.message}`);
reject(err);
}
resolve(
`${
this.configService.get('STORAGE_USE_SSL')?.toLocaleLowerCase() === 'true'
? 'https'
: 'http'
}://${this.configService.get('STORAGE_ENDPOINT')}:${this.configService.get('STORAGE_PORT')}/${
this.configService.get('MINIO_BUCKETNAME')
}/${filename}`,
`${this.useSSL ? 'https' : 'http'}://${this.storageEndpoint}:${this.storagePort}/${this.bucketName}/${filename}`,
);
},
);
});
}

async saveLocalFile(
destination: string,
filename: string,
file: any,
): Promise<string> {
async saveLocalFile(destination: string, filename: string, file: any): Promise<string> {
const uploadsDir = path.join(process.cwd(), destination);
const localFilePath = path.join(uploadsDir, filename);
if (!fs.existsSync(uploadsDir)) {
try {
// Create the directory
fs.mkdirSync(uploadsDir, { recursive: true });
this.logger.log(`Directory created at ${uploadsDir}`);
} catch (err) {
this.logger.error(`Error creating directory: ${err.message}`);
throw new InternalServerErrorException('File upload failed: directory creation error');
}
} else {
this.logger.log(`Directory already exists at ${uploadsDir}`);
Expand All @@ -89,48 +80,39 @@ export class FileUploadService {
return destination;
}

async upload(
file: any,
destination: string,
filename: string,
): Promise<string> {
async upload(file: any, destination: string, filename: string): Promise<string> {
try {
switch (this.configService.get<string>('STORAGE_MODE')?.toLowerCase()) {
case this.configService.get<string>('STORAGE_MODE.MINIO'):
this.logger.log('using minio');
return await this.uploadToMinio(filename, file);
default:
this.logger.log('writing to storage');
return await this.saveLocalFile(destination, filename, file);
if (this.useService) {
this.logger.log('Using Minio for file upload');
return await this.uploadToMinio(filename, file);
} else {
this.logger.log('Saving file locally');
return await this.saveLocalFile(destination, filename, file);
}
} catch (error) {
this.logger.error(`Error uploading file: ${error}`);
this.logger.error(`Error uploading file: ${error.message}`);
throw new InternalServerErrorException('File upload failed');
}
}

async download(destination: string): Promise<any> {
try {
if (this.useService) {
const fileStream = await this.storage.getObject(
this.configService.get<string>('STORAGE_CONTAINER_NAME'),
destination,
);
const fileStream = await this.storage.getObject(this.bucketName, destination);
return fileStream;
} else {
const localFilePath = path.join(process.cwd(), 'uploads', destination); // don't use __dirname here that'll point to the dist folder and not the top level folder containing the project (and the uploads folder)
const localFilePath = path.join(process.cwd(), 'uploads', destination);
if (fs.existsSync(localFilePath)) {
const fileStream = fs.createReadStream(localFilePath);
return fileStream;
}
else{
this.logger.error(`Error downloading file: File does not exist`);
return null;
const fileStream = fs.createReadStream(localFilePath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
return fileStream;
} else {
this.logger.error('File does not exist');
throw new InternalServerErrorException('File does not exist');
}
}
} catch (error) {
this.logger.error(`Error downloading file: ${error.message}`);
throw new InternalServerErrorException('File download failed');
}
}
}
}
65 changes: 21 additions & 44 deletions packages/common/test/file-upload/file-upload.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { InternalServerErrorException, Logger } from '@nestjs/common';
import { Client } from 'minio';
import * as fs from 'fs';
import * as path from 'path';
import { ConfigService } from '@nestjs/config';

jest.mock('minio');
jest.mock('fs');
jest.mock('path');
Expand All @@ -17,21 +19,27 @@ describe('FileUploadService', () => {
log: jest.fn(),
error: jest.fn(),
};
const mockConfigService = {
get: jest.fn((key: string) => {
const config = {
STORAGE_MODE: 'MINIO',
STORAGE_ENDPOINT: 'localhost',
STORAGE_PORT: '9000',
STORAGE_ACCESS_KEY: '5wmqDihWraT51LUgH2z1',
STORAGE_SECRET_KEY: '4AzloXYR22h15zPFjVuSmwKaCGPyUZKRovkSzOJW',
MINIO_BUCKETNAME: 'file-upload-test',
};
return config[key];
}),
};

beforeEach(() => {
process.env.STORAGE_MODE = 'minio';
process.env.STORAGE_ENDPOINT = 'localhost';
process.env.STORAGE_PORT = '9000';
process.env.STORAGE_ACCESS_KEY = 'access-key';
process.env.STORAGE_SECRET_KEY = 'secret-key';
process.env.MINIO_BUCKETNAME = 'bucket';

(Client as jest.Mock).mockImplementation(() => mockMinioClient);
jest.spyOn(Logger.prototype, 'log').mockImplementation(mockLogger.log);
jest.spyOn(Logger.prototype, 'error').mockImplementation(mockLogger.error);

service = new FileUploadService();
service['useSSL'] = false;
service = new FileUploadService(mockConfigService as unknown as ConfigService);
// Remove the assignment to 'useSSL'

(path.join as jest.Mock).mockImplementation((...paths) => paths.join('/'));
});
Expand Down Expand Up @@ -71,7 +79,7 @@ describe('FileUploadService', () => {
throw new Error('Directory creation error');
});

await service.saveLocalFile(mockDestination, mockFilename, mockFile);
await expect(service.saveLocalFile(mockDestination, mockFilename, mockFile)).rejects.toThrow(InternalServerErrorException);
expect(mockLogger.error).toHaveBeenCalledWith('Error creating directory: Directory creation error');
});
});
Expand All @@ -84,45 +92,14 @@ describe('FileUploadService', () => {
};
const filename = 'test.txt';
const destination = 'uploads';
const expectedUrl = `http://${process.env.STORAGE_ENDPOINT}:${process.env.STORAGE_PORT}/${process.env.MINIO_BUCKETNAME}/${filename}`;
const expectedUrl = `http://${mockConfigService.get('STORAGE_ENDPOINT')}:${mockConfigService.get('STORAGE_PORT')}/${mockConfigService.get('MINIO_BUCKETNAME')}/${filename}`;

jest.spyOn(service as any, 'uploadToMinio').mockResolvedValue(expectedUrl);

const result = await service.upload(file, destination, filename);
expect(result).toEqual(expectedUrl);
expect(service.uploadToMinio).toHaveBeenCalledWith(filename, file);
});

it('should save a file locally if STORAGE_MODE is not minio', async () => {
process.env.STORAGE_MODE = 'local';

const file = {
buffer: Buffer.from('test file'),
mimetype: 'text/plain',
};
const filename = 'test.txt';
const destination = 'uploads';
const expectedDestination = 'uploads';

jest.spyOn(service as any, 'saveLocalFile').mockResolvedValue(expectedDestination);

const result = await service.upload(file, destination, filename);
expect(result).toEqual(expectedDestination);
expect(service.saveLocalFile).toHaveBeenCalledWith(destination, filename, file);
});

it('should handle upload errors', async () => {
const file = {
buffer: Buffer.from('test file'),
mimetype: 'text/plain',
};
const filename = 'test.txt';
const destination = 'uploads';

jest.spyOn(service as any, 'uploadToMinio').mockRejectedValue(new Error('Upload error'));

await expect(service.upload(file, destination, filename)).rejects.toThrow(InternalServerErrorException);
expect(mockLogger.error).toHaveBeenCalledWith('Error uploading file: Error: Upload error');
});

});
});
});
3 changes: 2 additions & 1 deletion sample/06-file-upload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.0",
"@nestjs/config": "^3.2.2",
"@samagra-x/stencil": "^0.0.6",
"@types/multer": "^1.4.11",
"fastify": "^4.25.2",
"fastify-multer": "^2.0.3",
"fastify-multipart": "^5.4.0",
"minio": "^8.0.1",
"multer": "^1.4.5-lts.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
Expand Down
4 changes: 2 additions & 2 deletions sample/06-file-upload/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ describe('AppController', () => {
});

describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
it('should return "Hello World from sample/06-file-upload!"', () => {
expect(appController.getHello()).toBe('Hello World from sample/06-file-upload!');
});
});
});
2 changes: 1 addition & 1 deletion sample/06-file-upload/uploads/ayush
Original file line number Diff line number Diff line change
@@ -1 +1 @@
hello savio
Hello from file upload

0 comments on commit f70e993

Please sign in to comment.