mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
test: adicionar suite inicial de testes unitários com Vitest
Adiciona 81 testes unitários cobrindo backend e frontend: Backend (50 testes): - utils/version: isNewerVersion, parseMajorVersion (14 testes) - utils/misc: formatSpeed, toTitleCase, parseBoolean (17 testes) - utils/fs: determineFileType, matchesDevice, sanitizeFilename (14 testes) - validators/common: assertNotPrivateUrl - proteção SSRF (9 testes) Frontend (27 testes): - lib/classNames: concatenação condicional de classes (5 testes) - lib/util: capitalizeFirstLetter, formatBytes, extractFileName (12 testes) - hooks/useDiskDisplayData: getAllDiskDisplayItems, getPrimaryDiskInfo (6 testes) Infraestrutura: - Instala vitest, @testing-library/react, jsdom - Configura vitest.config.ts com aliases para ~ e #app - Script npm test:unit para rodar todos os testes Closes #491
This commit is contained in:
parent
f004c002a7
commit
7e0ae3ea83
67
admin/inertia/__tests__/lib/classNames.test.ts
Normal file
67
admin/inertia/__tests__/lib/classNames.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import classNames from '~/lib/classNames'
|
||||||
|
|
||||||
|
describe('classNames', () => {
|
||||||
|
it('deve juntar múltiplas strings com espaço', () => {
|
||||||
|
// Cenário
|
||||||
|
const classes = ['foo', 'bar', 'baz']
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = classNames(...classes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('foo bar baz')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve ignorar valores undefined', () => {
|
||||||
|
// Cenário
|
||||||
|
const classe1 = 'foo'
|
||||||
|
const classe2 = undefined
|
||||||
|
const classe3 = 'bar'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = classNames(classe1, classe2, classe3)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('foo bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve ignorar strings vazias', () => {
|
||||||
|
// Cenário
|
||||||
|
const classe1 = 'foo'
|
||||||
|
const classe2 = ''
|
||||||
|
const classe3 = 'bar'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = classNames(classe1, classe2, classe3)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('foo bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia quando chamado sem argumentos', () => {
|
||||||
|
// Cenário
|
||||||
|
// (nenhum argumento)
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = classNames()
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve lidar com mix de strings e undefined', () => {
|
||||||
|
// Cenário
|
||||||
|
const classe1 = undefined
|
||||||
|
const classe2 = 'active'
|
||||||
|
const classe3 = undefined
|
||||||
|
const classe4 = 'visible'
|
||||||
|
const classe5 = undefined
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = classNames(classe1, classe2, classe3, classe4, classe5)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('active visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
120
admin/inertia/__tests__/lib/useDiskDisplayData.test.ts
Normal file
120
admin/inertia/__tests__/lib/useDiskDisplayData.test.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { getAllDiskDisplayItems, getPrimaryDiskInfo } from '~/hooks/useDiskDisplayData'
|
||||||
|
import type { NomadDiskInfo } from '../../../types/system'
|
||||||
|
import type { Systeminformation } from 'systeminformation'
|
||||||
|
|
||||||
|
const mockDisks: NomadDiskInfo[] = [
|
||||||
|
{
|
||||||
|
name: 'sda',
|
||||||
|
totalSize: 1000000000,
|
||||||
|
totalUsed: 500000000,
|
||||||
|
percentUsed: 50,
|
||||||
|
model: 'Test Disk',
|
||||||
|
vendor: '',
|
||||||
|
rota: false,
|
||||||
|
tran: 'sata',
|
||||||
|
size: '1000000000',
|
||||||
|
filesystems: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockFsSize: Systeminformation.FsSizeData[] = [
|
||||||
|
{
|
||||||
|
fs: '/dev/sda1',
|
||||||
|
type: 'ext4',
|
||||||
|
size: 500000000,
|
||||||
|
used: 250000000,
|
||||||
|
available: 250000000,
|
||||||
|
use: 50,
|
||||||
|
mount: '/',
|
||||||
|
rw: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('getAllDiskDisplayItems', () => {
|
||||||
|
it('deve retornar items formatados com discos válidos', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks = mockDisks
|
||||||
|
const fsSize = undefined
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getAllDiskDisplayItems(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toHaveLength(1)
|
||||||
|
expect(resultado[0].label).toBe('sda')
|
||||||
|
expect(resultado[0].value).toBe(50)
|
||||||
|
expect(resultado[0].totalBytes).toBe(1000000000)
|
||||||
|
expect(resultado[0].usedBytes).toBe(500000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve usar fallback para fsSize quando discos são undefined', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks = undefined
|
||||||
|
const fsSize = mockFsSize
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getAllDiskDisplayItems(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toHaveLength(1)
|
||||||
|
expect(resultado[0].label).toBe('/dev/sda1')
|
||||||
|
expect(resultado[0].value).toBe(50)
|
||||||
|
expect(resultado[0].totalBytes).toBe(500000000)
|
||||||
|
expect(resultado[0].usedBytes).toBe(250000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar array vazio quando ambos são undefined', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks = undefined
|
||||||
|
const fsSize = undefined
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getAllDiskDisplayItems(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getPrimaryDiskInfo', () => {
|
||||||
|
it('deve retornar totalSize e totalUsed do maior disco', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks = mockDisks
|
||||||
|
const fsSize = undefined
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getPrimaryDiskInfo(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).not.toBeNull()
|
||||||
|
expect(resultado!.totalSize).toBe(1000000000)
|
||||||
|
expect(resultado!.totalUsed).toBe(500000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve usar fallback para fsSize quando não há discos', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks = undefined
|
||||||
|
const fsSize = mockFsSize
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getPrimaryDiskInfo(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).not.toBeNull()
|
||||||
|
expect(resultado!.totalSize).toBe(500000000)
|
||||||
|
expect(resultado!.totalUsed).toBe(250000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar null quando ambos são vazios', () => {
|
||||||
|
// Cenário
|
||||||
|
const disks: NomadDiskInfo[] = []
|
||||||
|
const fsSize: Systeminformation.FsSizeData[] = []
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = getPrimaryDiskInfo(disks, fsSize)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
186
admin/inertia/__tests__/lib/util.test.ts
Normal file
186
admin/inertia/__tests__/lib/util.test.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { capitalizeFirstLetter, formatBytes, extractFileName, generateRandomString } from '~/lib/util'
|
||||||
|
|
||||||
|
describe('capitalizeFirstLetter', () => {
|
||||||
|
it('deve capitalizar a primeira letra de uma string', () => {
|
||||||
|
// Cenário
|
||||||
|
const entrada = 'hello'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = capitalizeFirstLetter(entrada)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('Hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia para entrada vazia', () => {
|
||||||
|
// Cenário
|
||||||
|
const entrada = ''
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = capitalizeFirstLetter(entrada)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia para null', () => {
|
||||||
|
// Cenário
|
||||||
|
const entrada = null
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = capitalizeFirstLetter(entrada)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia para undefined', () => {
|
||||||
|
// Cenário
|
||||||
|
const entrada = undefined
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = capitalizeFirstLetter(entrada)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatBytes', () => {
|
||||||
|
it('deve retornar "0 Bytes" para 0 bytes', () => {
|
||||||
|
// Cenário
|
||||||
|
const bytes = 0
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = formatBytes(bytes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('0 Bytes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve formatar 1024 bytes como "1 KB"', () => {
|
||||||
|
// Cenário
|
||||||
|
const bytes = 1024
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = formatBytes(bytes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('1 KB')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve formatar 1048576 bytes como "1 MB"', () => {
|
||||||
|
// Cenário
|
||||||
|
const bytes = 1048576
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = formatBytes(bytes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('1 MB')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve formatar 1073741824 bytes como "1 GB"', () => {
|
||||||
|
// Cenário
|
||||||
|
const bytes = 1073741824
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = formatBytes(bytes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('1 GB')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve formatar 500 bytes como "500 Bytes"', () => {
|
||||||
|
// Cenário
|
||||||
|
const bytes = 500
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = formatBytes(bytes)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('500 Bytes')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('extractFileName', () => {
|
||||||
|
it('deve extrair nome do arquivo de caminho Unix', () => {
|
||||||
|
// Cenário
|
||||||
|
const caminho = '/home/user/file.txt'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = extractFileName(caminho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve extrair nome do arquivo de caminho Windows', () => {
|
||||||
|
// Cenário
|
||||||
|
const caminho = 'C:\\Users\\file.txt'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = extractFileName(caminho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar o próprio nome quando não há caminho', () => {
|
||||||
|
// Cenário
|
||||||
|
const caminho = 'file.txt'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = extractFileName(caminho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('file.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia para entrada vazia', () => {
|
||||||
|
// Cenário
|
||||||
|
const caminho = ''
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = extractFileName(caminho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('generateRandomString', () => {
|
||||||
|
it('deve gerar string com o tamanho especificado', () => {
|
||||||
|
// Cenário
|
||||||
|
const tamanho = 10
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = generateRandomString(tamanho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toHaveLength(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve retornar string vazia para tamanho 0', () => {
|
||||||
|
// Cenário
|
||||||
|
const tamanho = 0
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = generateRandomString(tamanho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deve conter apenas caracteres alfanuméricos', () => {
|
||||||
|
// Cenário
|
||||||
|
const tamanho = 100
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = generateRandomString(tamanho)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toMatch(/^[A-Za-z0-9]+$/)
|
||||||
|
})
|
||||||
|
})
|
||||||
1113
admin/package-lock.json
generated
1113
admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -10,6 +10,7 @@
|
||||||
"build": "node ace build",
|
"build": "node ace build",
|
||||||
"dev": "node ace serve --hmr",
|
"dev": "node ace serve --hmr",
|
||||||
"test": "node ace test",
|
"test": "node ace test",
|
||||||
|
"test:unit": "vitest run",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
|
@ -47,6 +48,8 @@
|
||||||
"@japa/runner": "^4.2.0",
|
"@japa/runner": "^4.2.0",
|
||||||
"@swc/core": "1.11.24",
|
"@swc/core": "1.11.24",
|
||||||
"@tanstack/eslint-plugin-query": "^5.81.2",
|
"@tanstack/eslint-plugin-query": "^5.81.2",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/dockerode": "^3.3.41",
|
"@types/dockerode": "^3.3.41",
|
||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.15.18",
|
||||||
|
|
@ -55,10 +58,12 @@
|
||||||
"@types/stopword": "^2.0.3",
|
"@types/stopword": "^2.0.3",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"hot-hook": "^0.4.0",
|
"hot-hook": "^0.4.0",
|
||||||
|
"jsdom": "^29.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"ts-node-maintained": "^10.9.5",
|
"ts-node-maintained": "^10.9.5",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.4.1"
|
"vite": "^6.4.1",
|
||||||
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.4.0",
|
"@adonisjs/auth": "^9.4.0",
|
||||||
|
|
|
||||||
64
admin/tests/unit/utils/fs.test.ts
Normal file
64
admin/tests/unit/utils/fs.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { determineFileType, matchesDevice, sanitizeFilename } from '#app/utils/fs'
|
||||||
|
|
||||||
|
describe('determineFileType', () => {
|
||||||
|
it('identifica arquivo JPG como image', () => {
|
||||||
|
expect(determineFileType('photo.jpg')).toBe('image')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifica arquivo PNG maiúsculo como image', () => {
|
||||||
|
expect(determineFileType('photo.PNG')).toBe('image')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifica arquivo PDF', () => {
|
||||||
|
expect(determineFileType('doc.pdf')).toBe('pdf')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifica arquivo Markdown como text', () => {
|
||||||
|
expect(determineFileType('readme.md')).toBe('text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifica arquivo TXT como text', () => {
|
||||||
|
expect(determineFileType('readme.txt')).toBe('text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifica arquivo ZIM', () => {
|
||||||
|
expect(determineFileType('wiki.zim')).toBe('zim')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna unknown para extensão não reconhecida', () => {
|
||||||
|
expect(determineFileType('archive.zip')).toBe('unknown')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('matchesDevice', () => {
|
||||||
|
it('corresponde dispositivo sda1 diretamente', () => {
|
||||||
|
expect(matchesDevice('/dev/sda1', 'sda1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('corresponde dispositivo nvme diretamente', () => {
|
||||||
|
expect(matchesDevice('/dev/nvme0n1p1', 'nvme0n1p1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('corresponde dispositivo LVM via device-mapper', () => {
|
||||||
|
expect(matchesDevice('/dev/mapper/ubuntu--vg-ubuntu--lv', 'ubuntu--lv')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('não corresponde dispositivos diferentes', () => {
|
||||||
|
expect(matchesDevice('/dev/sda1', 'sdb1')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sanitizeFilename', () => {
|
||||||
|
it('substitui espaços por underscores', () => {
|
||||||
|
expect(sanitizeFilename('hello world.txt')).toBe('hello_world.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('substitui caracteres especiais por underscores', () => {
|
||||||
|
expect(sanitizeFilename('file@#$.pdf')).toBe('file___.pdf')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mantém caracteres seguros inalterados', () => {
|
||||||
|
expect(sanitizeFilename('safe-file_name.zip')).toBe('safe-file_name.zip')
|
||||||
|
})
|
||||||
|
})
|
||||||
104
admin/tests/unit/utils/misc.test.ts
Normal file
104
admin/tests/unit/utils/misc.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { formatSpeed, toTitleCase, parseBoolean } from '#app/utils/misc'
|
||||||
|
|
||||||
|
describe('formatSpeed', () => {
|
||||||
|
it('formata bytes por segundo', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = formatSpeed(500)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('500 B/s')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formata kilobytes por segundo', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = formatSpeed(1024)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('1.0 KB/s')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formata megabytes por segundo', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = formatSpeed(1048576)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('1.0 MB/s')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formata zero bytes por segundo', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = formatSpeed(0)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('0 B/s')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('toTitleCase', () => {
|
||||||
|
it('converte texto minúsculo para title case', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = toTitleCase('hello world')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('Hello World')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converte texto maiúsculo para title case', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = toTitleCase('HELLO WORLD')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('Hello World')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converte texto com caixa mista para title case', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = toTitleCase('hELLO')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe('Hello')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseBoolean', () => {
|
||||||
|
it('retorna true para boolean true', () => {
|
||||||
|
expect(parseBoolean(true)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para boolean false', () => {
|
||||||
|
expect(parseBoolean(false)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true para string "true"', () => {
|
||||||
|
expect(parseBoolean('true')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para string "false"', () => {
|
||||||
|
expect(parseBoolean('false')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true para string "1"', () => {
|
||||||
|
expect(parseBoolean('1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para string "0"', () => {
|
||||||
|
expect(parseBoolean('0')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true para número 1', () => {
|
||||||
|
expect(parseBoolean(1)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para número 0', () => {
|
||||||
|
expect(parseBoolean(0)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para null', () => {
|
||||||
|
expect(parseBoolean(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para undefined', () => {
|
||||||
|
expect(parseBoolean(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
158
admin/tests/unit/utils/version.test.ts
Normal file
158
admin/tests/unit/utils/version.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { isNewerVersion, parseMajorVersion } from '#app/utils/version'
|
||||||
|
|
||||||
|
describe('isNewerVersion', () => {
|
||||||
|
it('retorna true quando major é maior', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '2.0.0'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true quando minor é maior', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.1.0'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true quando patch é maior', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.0.1'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false quando major é menor', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.0.0'
|
||||||
|
const v2 = '2.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false quando versões são iguais', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.0.0'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true com prefixo v', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = 'v2.0.0'
|
||||||
|
const v2 = 'v1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna false para pre-release sem flag includePreReleases', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '2.0.0-rc.1'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true para pre-release com flag includePreReleases', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '2.0.0-rc.1'
|
||||||
|
const v2 = '1.0.0'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2, true)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true quando GA é comparado com RC da mesma versão', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.0.0'
|
||||||
|
const v2 = '1.0.0-rc.1'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna true quando RC maior é comparado com RC menor', () => {
|
||||||
|
// Cenário
|
||||||
|
const v1 = '1.0.0-rc.2'
|
||||||
|
const v2 = '1.0.0-rc.1'
|
||||||
|
|
||||||
|
// Ação
|
||||||
|
const resultado = isNewerVersion(v1, v2, true)
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseMajorVersion', () => {
|
||||||
|
it('extrai major de tag com prefixo v', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = parseMajorVersion('v3.8.1')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extrai major de tag sem prefixo v', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = parseMajorVersion('10.19.4')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna 0 para tag inválida', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = parseMajorVersion('invalid')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retorna 0 para versão 0.x.x', () => {
|
||||||
|
// Cenário / Ação
|
||||||
|
const resultado = parseMajorVersion('v0.1.0')
|
||||||
|
|
||||||
|
// Validação
|
||||||
|
expect(resultado).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
56
admin/tests/unit/validators/common.test.ts
Normal file
56
admin/tests/unit/validators/common.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { assertNotPrivateUrl } from '#app/validators/common'
|
||||||
|
|
||||||
|
describe('assertNotPrivateUrl', () => {
|
||||||
|
it('permite URL pública HTTPS', () => {
|
||||||
|
// Cenário
|
||||||
|
const url = 'https://example.com/file.zim'
|
||||||
|
|
||||||
|
// Ação / Validação
|
||||||
|
expect(() => assertNotPrivateUrl(url)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('permite URL RFC1918 192.168.x.x (LAN appliance)', () => {
|
||||||
|
// Cenário
|
||||||
|
const url = 'http://192.168.1.100:8080/file.zim'
|
||||||
|
|
||||||
|
// Ação / Validação
|
||||||
|
expect(() => assertNotPrivateUrl(url)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('permite URL RFC1918 10.x.x.x (LAN appliance)', () => {
|
||||||
|
// Cenário
|
||||||
|
const url = 'http://10.0.0.1/file.zim'
|
||||||
|
|
||||||
|
// Ação / Validação
|
||||||
|
expect(() => assertNotPrivateUrl(url)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia localhost', () => {
|
||||||
|
// Cenário
|
||||||
|
const url = 'http://localhost/file'
|
||||||
|
|
||||||
|
// Ação / Validação
|
||||||
|
expect(() => assertNotPrivateUrl(url)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia 127.0.0.1 (loopback)', () => {
|
||||||
|
expect(() => assertNotPrivateUrl('http://127.0.0.1/file')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia 127.0.0.2 (loopback alternativo)', () => {
|
||||||
|
expect(() => assertNotPrivateUrl('http://127.0.0.2/file')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia 0.0.0.0', () => {
|
||||||
|
expect(() => assertNotPrivateUrl('http://0.0.0.0/file')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia endereço de metadados cloud 169.254.169.254', () => {
|
||||||
|
expect(() => assertNotPrivateUrl('http://169.254.169.254/metadata')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bloqueia IPv6 loopback [::1]', () => {
|
||||||
|
expect(() => assertNotPrivateUrl('http://[::1]/file')).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
20
admin/vitest.config.ts
Normal file
20
admin/vitest.config.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: [
|
||||||
|
'inertia/__tests__/**/*.test.ts',
|
||||||
|
'tests/unit/**/*.test.ts',
|
||||||
|
],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': resolve(__dirname, 'inertia'),
|
||||||
|
'#app': resolve(__dirname, 'app'),
|
||||||
|
'#validators': resolve(__dirname, 'app/validators'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user