Skip to content

Commit

Permalink
fix(profile-pic): implemented fix for profile picture
Browse files Browse the repository at this point in the history
  • Loading branch information
King-Mikaelson committed Aug 12, 2024
1 parent 819eb27 commit b9e68d4
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 41 deletions.
2 changes: 1 addition & 1 deletion src/helpers/app-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import * as path from 'path';
export const MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024;
export const VALID_UPLOADS_MIME_TYPES = ['image/jpeg', 'image/png'];
export const BASE_URL = "https://staging.api-nestjs.boilerplate.hng.tech";
export const PROFILE_PHOTO_UPLOADS = path.join(__dirname, '..', 'uploads')
export const PROFILE_PHOTO_UPLOADS = path.join(__dirname, '..', 'uploads')
53 changes: 28 additions & 25 deletions src/modules/profile/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
HttpStatus,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
} from '@nestjs/common';
import { Profile } from './entities/profile.entity';
Expand All @@ -11,12 +12,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../user/entities/user.entity';
import { UpdateProfileDto } from './dto/update-profile.dto';
import * as sharp from 'sharp';
import * as fs from 'fs/promises';
import * as fs from 'fs';
import * as path from 'path';
import { CustomHttpException } from '../../helpers/custom-http-filter';
import * as SYS_MSG from '../../helpers/SystemMessages';
import { UploadProfilePicDto } from './dto/upload-profile-pic.dto';
import { PROFILE_PHOTO_UPLOADS } from '../../helpers/app-constants';
import { pipeline, Readable } from 'stream';

@Injectable()
export class ProfileService {
Expand All @@ -41,7 +43,7 @@ export class ProfileService {

const userProfile = await this.userRepository.findOne({
where: { id: userId },
relations: ['profile']
relations: ['profile'],
});

const profile = userProfile.profile;
Expand Down Expand Up @@ -130,7 +132,7 @@ export class ProfileService {
throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND);
}

const profile = user.profile
const profile = user.profile;
if (!profile) {
throw new CustomHttpException(SYS_MSG.PROFILE_NOT_FOUND, HttpStatus.NOT_FOUND);
}
Expand All @@ -139,8 +141,8 @@ export class ProfileService {
const previousFilePath = path.join(this.uploadsDir, path.basename(profile.profile_pic_url));

try {
await fs.access(previousFilePath);
await fs.unlink(previousFilePath);
await fs.promises.access(previousFilePath);
await fs.promises.unlink(previousFilePath);
} catch (error) {
if (error.code === 'ENOENT') {
console.error(SYS_MSG.PROFILE_PIC_NOT_FOUND, previousFilePath);
Expand All @@ -150,38 +152,39 @@ export class ProfileService {
}
}


const fileExtension = path.extname(uploadProfilePicDto.file.originalname);
const fileName = `${userId}${fileExtension}`;
const filePath = path.join(this.uploadsDir, fileName);

const writeStream = await fs.open(filePath, 'w');
try {
await writeStream.writeFile(uploadProfilePicDto.file.buffer);
await writeStream.close();
} catch (error) {
await writeStream.close();
throw new CustomHttpException(SYS_MSG.FILE_SAVE_ERROR, HttpStatus.INTERNAL_SERVER_ERROR);
}

const fileStream = Readable.from(uploadProfilePicDto.file.buffer);
const writeStream = fs.createWriteStream(filePath);

await sharp(uploadProfilePicDto.file.buffer).resize({ width: 200, height: 200 }).toFile(filePath);
return new Promise((resolve, reject) => {
pipeline(fileStream, writeStream, async err => {
if (err) {
Logger.error(SYS_MSG.FILE_SAVE_ERROR, err.stack);
reject(new CustomHttpException(SYS_MSG.FILE_SAVE_ERROR, HttpStatus.INTERNAL_SERVER_ERROR));
} else {
await sharp(uploadProfilePicDto.file.buffer).resize({ width: 200, height: 200 }).toFile(filePath);

profile.profile_pic_url = `${baseUrl}/uploads/${fileName}`;
profile.profile_pic_url = `${baseUrl}/uploads/${fileName}`;

await this.profileRepository.update(profile.id, profile);
const updatedProfile = await this.profileRepository.findOne({ where: { id: profile.id } });
await this.profileRepository.update(profile.id, profile);
const updatedProfile = await this.profileRepository.findOne({ where: { id: profile.id } });
resolve({
status: HttpStatus.OK,
message: SYS_MSG.PICTURE_UPDATED,
data: { profile_picture_url: updatedProfile.profile_pic_url },
});
}
});
});

return {
status: HttpStatus.OK,
message: SYS_MSG.PICTURE_UPDATED,
data: { profile_picture_url: updatedProfile.profile_pic_url },
};
}

private async createUploadsDirectory() {
try {
await fs.mkdir(this.uploadsDir, { recursive: true });
await fs.promises.mkdir(this.uploadsDir, { recursive: true });
} catch (error) {
console.error(SYS_MSG.ERROR_DIRECTORY, error);
}
Expand Down
20 changes: 5 additions & 15 deletions src/modules/profile/tests/profile.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ import { Profile } from '../entities/profile.entity';
import { NotFoundException, InternalServerErrorException, HttpStatus } from '@nestjs/common';
import { User } from '../../user/entities/user.entity';
import { UpdateProfileDto } from '../dto/update-profile.dto';
import * as path from 'path';
import * as fs from 'fs/promises';
import { FileHandle } from 'fs/promises';
import * as fs from 'fs';
import * as sharp from 'sharp';
import { CustomHttpException } from '../../../helpers/custom-http-filter';
import { PICTURE_UPDATED } from '../../../helpers/SystemMessages';
import { mockUser } from '../../../modules/invite/mocks/mockUser';
import { mockUserWithProfile } from '../mocks/mockUser';
jest.mock('fs/promises');
jest.mock('sharp');
describe('ProfileService', () => {
let service: ProfileService;
Expand Down Expand Up @@ -193,10 +190,7 @@ describe('ProfileService', () => {
originalname: 'test.jpg',
};
const mockUploadProfilePicDto = { file: mockFile as any };
const mockFileHandle = {
writeFile: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
} as unknown as FileHandle;


it('should throw an exception if no file is provided', async () => {
await expect(service.uploadProfilePicture(userId, { file: null }, baseUrl)).rejects.toThrow(CustomHttpException);
Expand All @@ -222,7 +216,6 @@ describe('ProfileService', () => {
});

it('should delete previous profile picture if it exists', async () => {
jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle);

jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);

Expand All @@ -235,8 +228,8 @@ describe('ProfileService', () => {
toFile: jest.fn().mockResolvedValue(undefined),
} as any);

const mockUnlink = jest.spyOn(fs, 'unlink').mockResolvedValue(undefined);
const mockAccess = jest.spyOn(fs, 'access').mockResolvedValue(undefined);
const mockUnlink = jest.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined);
const mockAccess = jest.spyOn(fs.promises, 'access').mockResolvedValue(undefined);

await service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl);

Expand All @@ -245,7 +238,6 @@ describe('ProfileService', () => {
});

it('should handle non-existent previous profile picture', async () => {
jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle);

const mockResult: UpdateResult = {
generatedMaps: [],
Expand All @@ -256,7 +248,7 @@ describe('ProfileService', () => {
jest.spyOn(profileRepository, 'update').mockResolvedValue(mockResult);
jest.spyOn(profileRepository, 'findOne').mockResolvedValue(mockUser.profile);

(fs.access as jest.Mock).mockRejectedValue({ code: 'ENOENT' });
(fs.promises.access as jest.Mock).mockRejectedValue({ code: 'ENOENT' });

(sharp as jest.MockedFunction<typeof sharp>).mockReturnValue({
resize: jest.fn().mockReturnThis(),
Expand All @@ -268,8 +260,6 @@ describe('ProfileService', () => {

it('should save new profile picture and update profile', async () => {

jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle);

jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);

(sharp as jest.MockedFunction<typeof sharp>).mockReturnValue({
Expand Down
1 change: 1 addition & 0 deletions src/uploads/testUserId.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions uploads/testUserId.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b9e68d4

Please sign in to comment.