chore: initial commit
This commit is contained in:
7
src/tools/utils/__test__/capitalize.test.ts
Normal file
7
src/tools/utils/__test__/capitalize.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { capitalize } from '../capitalize'
|
||||
|
||||
test('/tools/utils/capitalize', () => {
|
||||
expect(capitalize('hello world')).toBe('Hello world')
|
||||
expect('Test').toBe('Test')
|
||||
expect('TEST').toBe('TEST')
|
||||
})
|
124
src/tools/utils/__test__/deleteFiles.test.ts
Normal file
124
src/tools/utils/__test__/deleteFiles.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import fsMock from 'mock-fs'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
|
||||
import {
|
||||
deleteAllFilesInDirectory,
|
||||
deleteFile,
|
||||
deleteMessages
|
||||
} from '../deleteFiles'
|
||||
import { messagesFilePath, usersLogoPath } from '../../configurations/constants'
|
||||
import Message from '../../../models/Message'
|
||||
import Guild from '../../../models/Guild'
|
||||
import Member from '../../../models/Member'
|
||||
import Channel from '../../../models/Channel'
|
||||
import User from '../../../models/User'
|
||||
|
||||
const fs = fsWithCallbacks.promises
|
||||
|
||||
describe('/tools/utils/deleteFiles - deleteAllFilesInDirectory', () => {
|
||||
it('delete all the files expect the directories', async () => {
|
||||
fsMock({
|
||||
'/files': {
|
||||
'default.png': '',
|
||||
'user-logo.png': '',
|
||||
'user-logo.jpg': '',
|
||||
directory: {
|
||||
file: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
await deleteAllFilesInDirectory('/files')
|
||||
const directoryContent = await fs.readdir('/files')
|
||||
expect(directoryContent.length).toEqual(1)
|
||||
expect(directoryContent[0]).toEqual('directory')
|
||||
})
|
||||
|
||||
it('delete all the files with all the directories recursively', async () => {
|
||||
fsMock({
|
||||
'/files': {
|
||||
'default.png': '',
|
||||
'user-logo.png': '',
|
||||
'user-logo.jpg': '',
|
||||
directory: {
|
||||
file: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
await deleteAllFilesInDirectory('/files', true)
|
||||
const filesDirectoryContent = await fs.readdir('/files')
|
||||
const directoryContent = await fs.readdir('/files/directory')
|
||||
expect(filesDirectoryContent.length).toEqual(1)
|
||||
expect(directoryContent.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('/tools/utils/deleteFiles - deleteFile', () => {
|
||||
it('should delete the file', async () => {
|
||||
fsMock({
|
||||
[usersLogoPath.filePath]: {
|
||||
'logo.png': ''
|
||||
}
|
||||
})
|
||||
await deleteFile({
|
||||
basePath: usersLogoPath,
|
||||
valueSavedInDatabase: `${usersLogoPath.name}/logo.png`
|
||||
})
|
||||
const directoryContent = await fs.readdir(usersLogoPath.filePath)
|
||||
expect(directoryContent.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should not delete the default file', async () => {
|
||||
fsMock({
|
||||
[usersLogoPath.filePath]: {
|
||||
'logo.png': '',
|
||||
'default.png': ''
|
||||
}
|
||||
})
|
||||
await deleteFile({
|
||||
basePath: usersLogoPath,
|
||||
valueSavedInDatabase: `${usersLogoPath.name}/default.png`
|
||||
})
|
||||
const directoryContent = await fs.readdir(usersLogoPath.filePath)
|
||||
expect(directoryContent.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('/tools/utils/deleteFiles - deleteMessages', () => {
|
||||
it('should delete every messages and files', async () => {
|
||||
fsMock({
|
||||
[messagesFilePath.filePath]: {
|
||||
'logo.png': '',
|
||||
'random-file.mp3': '',
|
||||
'file-without-message': ''
|
||||
}
|
||||
})
|
||||
const user = await User.create({ name: 'John' })
|
||||
const guild = await Guild.create({ name: 'testing' })
|
||||
const channel = await Channel.create({
|
||||
name: 'general',
|
||||
isDefault: true,
|
||||
guildId: guild.id
|
||||
})
|
||||
const member = await Member.create({
|
||||
userId: user.id,
|
||||
guildId: guild.id,
|
||||
isOwner: true,
|
||||
lastVisitedChannelId: channel.id
|
||||
})
|
||||
const messagesToCreate = [
|
||||
{ value: `${messagesFilePath.name}/logo.png`, type: 'file' },
|
||||
{ value: `${messagesFilePath.name}/random-file.mp3`, type: 'file' }
|
||||
]
|
||||
const messages = messagesToCreate.map(async (message) => {
|
||||
return await Message.create({
|
||||
value: message.value,
|
||||
type: message.type,
|
||||
memberId: member.id,
|
||||
channelId: channel.id
|
||||
})
|
||||
})
|
||||
await deleteMessages(await Promise.all(messages))
|
||||
const directoryContent = await fs.readdir(messagesFilePath.filePath)
|
||||
expect(directoryContent.length).toEqual(1)
|
||||
})
|
||||
})
|
9
src/tools/utils/__test__/deleteObjectAttributes.test.ts
Normal file
9
src/tools/utils/__test__/deleteObjectAttributes.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { deleteObjectAttributes } from '../deleteObjectAttributes'
|
||||
|
||||
test('/tools/utils/deleteObjectAttributes', () => {
|
||||
const object = { attribute1: 'value1', attribute2: 'value2' }
|
||||
const hiddenObjectAttributes = ['attribute2']
|
||||
const result = deleteObjectAttributes(object, hiddenObjectAttributes)
|
||||
expect(result.attribute1).toEqual('value1')
|
||||
expect(result.attribute2).not.toBeDefined()
|
||||
})
|
8
src/tools/utils/__test__/parseIntOrDefaultValue.test.ts
Normal file
8
src/tools/utils/__test__/parseIntOrDefaultValue.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { parseIntOrDefaultValue } from '../parseIntOrDefaultValue'
|
||||
|
||||
test('/tools/utils/parseIntOrDefaultValue', () => {
|
||||
expect(parseIntOrDefaultValue('12', 10)).toEqual(12)
|
||||
expect(parseIntOrDefaultValue('shshsksk2', 10)).toEqual(10)
|
||||
expect(parseIntOrDefaultValue('', 10)).toEqual(10)
|
||||
expect(parseIntOrDefaultValue(' ', 10)).toEqual(10)
|
||||
})
|
30
src/tools/utils/__test__/random.test.ts
Normal file
30
src/tools/utils/__test__/random.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {
|
||||
randomCharacter,
|
||||
randomInteger,
|
||||
randomString,
|
||||
alphabet
|
||||
} from '../random'
|
||||
|
||||
describe('/tools/utils/random', () => {
|
||||
test('randomInteger', () => {
|
||||
const min = 1
|
||||
const max = 100
|
||||
const result = randomInteger(min, max)
|
||||
const isInteger = result % 1 === 0
|
||||
expect(isInteger).toBeTruthy()
|
||||
expect(result).toBeGreaterThanOrEqual(min)
|
||||
expect(result).toBeLessThanOrEqual(max)
|
||||
})
|
||||
|
||||
test('randomCharacter', () => {
|
||||
const result = randomCharacter()
|
||||
expect(result.length).toEqual(1)
|
||||
expect(alphabet.split('').includes(result))
|
||||
})
|
||||
|
||||
test('randomString', () => {
|
||||
const length = 7
|
||||
const result = randomString(length)
|
||||
expect(result.length).toEqual(length)
|
||||
})
|
||||
})
|
106
src/tools/utils/__test__/uploadImage.test.ts
Normal file
106
src/tools/utils/__test__/uploadImage.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import fsMock from 'mock-fs'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import { UploadedFile } from 'express-fileupload'
|
||||
|
||||
import { uploadImage } from '../uploadImage'
|
||||
import { PayloadTooLargeError } from '../../errors/PayloadTooLargeError'
|
||||
import { tempPath, usersLogoPath } from '../../configurations/constants'
|
||||
import { BadRequestError } from '../../errors/BadRequestError'
|
||||
|
||||
const fs = fsWithCallbacks.promises
|
||||
|
||||
const imagesPath = usersLogoPath.filePath
|
||||
|
||||
const getImage = (
|
||||
props: { truncated?: boolean, mimetype?: string } = {}
|
||||
): UploadedFile => {
|
||||
const { truncated = false, mimetype = 'image/png' } = props
|
||||
return {
|
||||
name: 'logo',
|
||||
mv: jest.fn(),
|
||||
encoding: 'utf-8',
|
||||
mimetype,
|
||||
data: Buffer.from([]),
|
||||
tempFilePath: '/temp/logo.png',
|
||||
truncated,
|
||||
size: 1024,
|
||||
md5: '12345abcd'
|
||||
}
|
||||
}
|
||||
|
||||
describe('/tools/utils/uploadImage', () => {
|
||||
it('should succeeds and save the image', async () => {
|
||||
fsMock({
|
||||
[tempPath]: {
|
||||
'logo.png': ''
|
||||
},
|
||||
[imagesPath]: {
|
||||
'logo.png': ''
|
||||
}
|
||||
})
|
||||
const image = getImage()
|
||||
const result = await uploadImage({
|
||||
image,
|
||||
propertyName: 'logo',
|
||||
oldImage: '/images/logo.png',
|
||||
imagesPath
|
||||
})
|
||||
expect(result).not.toBeNull()
|
||||
expect(image.mv).toHaveBeenCalled()
|
||||
const directoryContent = await fs.readdir(tempPath)
|
||||
expect(directoryContent.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should returns null with undefined image file(s)', async () => {
|
||||
const result = await uploadImage({
|
||||
image: [getImage(), getImage()],
|
||||
propertyName: 'logo',
|
||||
oldImage: '/images/logo.png',
|
||||
imagesPath
|
||||
})
|
||||
const result2 = await uploadImage({
|
||||
image: undefined,
|
||||
propertyName: 'logo',
|
||||
oldImage: '/images/logo.png',
|
||||
imagesPath
|
||||
})
|
||||
expect(result).toBeNull()
|
||||
expect(result2).toBeNull()
|
||||
})
|
||||
|
||||
it('should fails if the file is over the size limit', async () => {
|
||||
fsMock({
|
||||
[tempPath]: {
|
||||
'logo.png': ''
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
uploadImage({
|
||||
image: getImage({ truncated: true }),
|
||||
propertyName: 'logo',
|
||||
oldImage: '/images/logo.png',
|
||||
imagesPath
|
||||
})
|
||||
).rejects.toThrow(PayloadTooLargeError)
|
||||
const directoryContent = await fs.readdir(tempPath)
|
||||
expect(directoryContent.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should fails if the file is not an image', async () => {
|
||||
fsMock({
|
||||
[tempPath]: {
|
||||
'logo.png': ''
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
uploadImage({
|
||||
image: getImage({ mimetype: 'text/html' }),
|
||||
propertyName: 'logo',
|
||||
oldImage: '/images/logo.png',
|
||||
imagesPath
|
||||
})
|
||||
).rejects.toThrow(BadRequestError)
|
||||
const directoryContent = await fs.readdir(tempPath)
|
||||
expect(directoryContent.length).toEqual(0)
|
||||
})
|
||||
})
|
4
src/tools/utils/capitalize.ts
Normal file
4
src/tools/utils/capitalize.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/** converts the first character of the string to capital (uppercase) letter */
|
||||
export function capitalize (s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
57
src/tools/utils/deleteFiles.ts
Normal file
57
src/tools/utils/deleteFiles.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
guildsIconPath,
|
||||
messagesFilePath,
|
||||
usersLogoPath
|
||||
} from '../configurations/constants'
|
||||
import Message from '../../models/Message'
|
||||
|
||||
const fs = fsWithCallbacks.promises
|
||||
|
||||
export const deleteAllFilesInDirectory = async (
|
||||
directoryPath: string,
|
||||
isRecursive: boolean = false
|
||||
): Promise<void> => {
|
||||
const files = await fs.readdir(directoryPath)
|
||||
for (const file of files) {
|
||||
const filePath = path.resolve(directoryPath, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
if (stats.isFile()) {
|
||||
await fs.unlink(filePath)
|
||||
} else if (isRecursive && stats.isDirectory()) {
|
||||
await deleteAllFilesInDirectory(filePath, isRecursive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BasePath =
|
||||
| typeof guildsIconPath
|
||||
| typeof usersLogoPath
|
||||
| typeof messagesFilePath
|
||||
|
||||
export const deleteFile = async (options: {
|
||||
basePath: BasePath
|
||||
/** @example '/uploads/users/logo.png' */
|
||||
valueSavedInDatabase: string
|
||||
}): Promise<void> => {
|
||||
const { basePath, valueSavedInDatabase: value } = options
|
||||
if (value !== `${basePath.name}/default.png`) {
|
||||
const filePath = value.split('/')
|
||||
const filename = filePath[filePath.length - 1]
|
||||
await fs.unlink(path.join(basePath.filePath, filename))
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteMessages = async (messages: Message[]): Promise<void> => {
|
||||
for (const message of messages) {
|
||||
if (message.type === 'file') {
|
||||
await deleteFile({
|
||||
basePath: messagesFilePath,
|
||||
valueSavedInDatabase: message.value
|
||||
})
|
||||
}
|
||||
await message.destroy()
|
||||
}
|
||||
}
|
12
src/tools/utils/deleteObjectAttributes.ts
Normal file
12
src/tools/utils/deleteObjectAttributes.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ObjectAny } from '../../typings/utils'
|
||||
|
||||
export const deleteObjectAttributes = (
|
||||
object: ObjectAny,
|
||||
attributesToDelete: readonly string[]
|
||||
): ObjectAny => {
|
||||
const map = new Map(Object.entries(object))
|
||||
for (const attribute of attributesToDelete) {
|
||||
map.delete(attribute)
|
||||
}
|
||||
return Object.fromEntries(map)
|
||||
}
|
11
src/tools/utils/parseIntOrDefaultValue.ts
Normal file
11
src/tools/utils/parseIntOrDefaultValue.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/** returns the defaultValue provided, if parseInt(value) return NaN */
|
||||
export function parseIntOrDefaultValue (
|
||||
value: string | undefined,
|
||||
defaultValue: number
|
||||
): number {
|
||||
const valueInteger = parseInt(value as string, 10)
|
||||
if (value != null && !isNaN(valueInteger)) {
|
||||
return valueInteger
|
||||
}
|
||||
return defaultValue
|
||||
}
|
16
src/tools/utils/random.ts
Normal file
16
src/tools/utils/random.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const alphabet =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
|
||||
export function randomInteger (min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
export function randomCharacter (): string {
|
||||
return alphabet.charAt(randomInteger(0, alphabet.length - 1))
|
||||
}
|
||||
|
||||
export function randomString (length: number): string {
|
||||
return Array.from({ length })
|
||||
.map(randomCharacter)
|
||||
.join('')
|
||||
}
|
58
src/tools/utils/uploadImage.ts
Normal file
58
src/tools/utils/uploadImage.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { UploadedFile } from 'express-fileupload'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import path from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import {
|
||||
commonErrorsMessages,
|
||||
supportedImageMimetype,
|
||||
tempPath
|
||||
} from '../configurations/constants'
|
||||
import { deleteAllFilesInDirectory } from './deleteFiles'
|
||||
import { BadRequestError } from '../errors/BadRequestError'
|
||||
import { PayloadTooLargeError } from '../errors/PayloadTooLargeError'
|
||||
|
||||
const fs = fsWithCallbacks.promises
|
||||
|
||||
interface UploadImageOptions {
|
||||
image: UploadedFile | UploadedFile[] | undefined
|
||||
propertyName: string
|
||||
oldImage: string
|
||||
imagesPath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Handle upload of an image
|
||||
* @returns the complete image name if success otherwise null
|
||||
*/
|
||||
export const uploadImage = async (
|
||||
options: UploadImageOptions
|
||||
): Promise<string | null> => {
|
||||
const { image, propertyName, oldImage, imagesPath } = options
|
||||
if (image != null && !Array.isArray(image)) {
|
||||
if (image.truncated) {
|
||||
await deleteAllFilesInDirectory(tempPath)
|
||||
throw new PayloadTooLargeError(
|
||||
commonErrorsMessages.image.tooLarge(propertyName)
|
||||
)
|
||||
}
|
||||
if (!supportedImageMimetype.includes(image.mimetype)) {
|
||||
await deleteAllFilesInDirectory(tempPath)
|
||||
throw new BadRequestError(
|
||||
commonErrorsMessages.image.validType(propertyName)
|
||||
)
|
||||
}
|
||||
const splitedMimetype = image.mimetype.split('/')
|
||||
const imageExtension = splitedMimetype[1]
|
||||
const completeImageName = `${uuidv4()}.${imageExtension}`
|
||||
const oldImagePath = oldImage.split('/')
|
||||
const oldImageName = oldImagePath[oldImagePath.length - 1]
|
||||
if (!oldImageName.startsWith('default')) {
|
||||
await fs.unlink(path.join(imagesPath, oldImageName))
|
||||
}
|
||||
await image.mv(path.join(imagesPath, completeImageName))
|
||||
await deleteAllFilesInDirectory(tempPath)
|
||||
return completeImageName
|
||||
}
|
||||
return null
|
||||
}
|
Reference in New Issue
Block a user