chore: initial commit

This commit is contained in:
Divlo
2021-10-24 04:06:16 +02:00
commit 714cc643ba
260 changed files with 40783 additions and 0 deletions

View 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')
})

View 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)
})
})

View 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()
})

View 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)
})

View 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)
})
})

View 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)
})
})

View 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)
}

View 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()
}
}

View 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)
}

View 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
View 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('')
}

View 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
}