From 7e0ae3ea83542f2b15d52929efc92951145a094c Mon Sep 17 00:00:00 2001 From: LuisMIguelFurlanettoSousa Date: Mon, 23 Mar 2026 11:53:55 -0300 Subject: [PATCH] =?UTF-8?q?test:=20adicionar=20suite=20inicial=20de=20test?= =?UTF-8?q?es=20unit=C3=A1rios=20com=20Vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../inertia/__tests__/lib/classNames.test.ts | 67 + .../__tests__/lib/useDiskDisplayData.test.ts | 120 ++ admin/inertia/__tests__/lib/util.test.ts | 186 +++ admin/package-lock.json | 1113 ++++++++++++++++- admin/package.json | 7 +- admin/tests/unit/utils/fs.test.ts | 64 + admin/tests/unit/utils/misc.test.ts | 104 ++ admin/tests/unit/utils/version.test.ts | 158 +++ admin/tests/unit/validators/common.test.ts | 56 + admin/vitest.config.ts | 20 + 10 files changed, 1893 insertions(+), 2 deletions(-) create mode 100644 admin/inertia/__tests__/lib/classNames.test.ts create mode 100644 admin/inertia/__tests__/lib/useDiskDisplayData.test.ts create mode 100644 admin/inertia/__tests__/lib/util.test.ts create mode 100644 admin/tests/unit/utils/fs.test.ts create mode 100644 admin/tests/unit/utils/misc.test.ts create mode 100644 admin/tests/unit/utils/version.test.ts create mode 100644 admin/tests/unit/validators/common.test.ts create mode 100644 admin/vitest.config.ts diff --git a/admin/inertia/__tests__/lib/classNames.test.ts b/admin/inertia/__tests__/lib/classNames.test.ts new file mode 100644 index 0000000..b72cae8 --- /dev/null +++ b/admin/inertia/__tests__/lib/classNames.test.ts @@ -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') + }) +}) diff --git a/admin/inertia/__tests__/lib/useDiskDisplayData.test.ts b/admin/inertia/__tests__/lib/useDiskDisplayData.test.ts new file mode 100644 index 0000000..9a5d992 --- /dev/null +++ b/admin/inertia/__tests__/lib/useDiskDisplayData.test.ts @@ -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() + }) +}) diff --git a/admin/inertia/__tests__/lib/util.test.ts b/admin/inertia/__tests__/lib/util.test.ts new file mode 100644 index 0000000..32546e6 --- /dev/null +++ b/admin/inertia/__tests__/lib/util.test.ts @@ -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]+$/) + }) +}) diff --git a/admin/package-lock.json b/admin/package-lock.json index ac4b5b9..af0d03e 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -81,6 +81,8 @@ "@japa/runner": "^4.2.0", "@swc/core": "1.11.24", "@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/luxon": "^3.6.2", "@types/node": "^22.15.18", @@ -89,10 +91,12 @@ "@types/stopword": "^2.0.3", "eslint": "^9.26.0", "hot-hook": "^0.4.0", + "jsdom": "^29.0.1", "prettier": "^3.5.3", "ts-node-maintained": "^10.9.5", "typescript": "~5.8.3", - "vite": "^6.4.1" + "vite": "^6.4.1", + "vitest": "^4.1.0" } }, "node_modules/@adobe/css-tools": { @@ -856,6 +860,67 @@ "node": ">=4" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1092,6 +1157,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1190,6 +1265,19 @@ "node": ">=20.11.1" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", @@ -1281,6 +1369,146 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -1899,6 +2127,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@faker-js/faker": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", @@ -4312,6 +4558,13 @@ "devOptional": true, "license": "CC0-1.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.7.1.tgz", @@ -5008,6 +5261,120 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -5080,6 +5447,14 @@ "integrity": "sha512-Y1JgQoshbcxEwmajeWpJibBmoBlGuEq38ICKmWQ5dS+ESqY0J0757rWcHAQgiB74J1vf/DxHkt8veBRSKTAjJQ==", "license": "ISC" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5992,6 +6367,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -6162,6 +6650,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", @@ -6397,6 +6895,16 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -7404,6 +7912,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -7416,6 +7938,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -7428,6 +7957,68 @@ "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -7460,6 +8051,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", @@ -7648,6 +8246,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8433,6 +9039,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8526,6 +9142,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -9630,6 +10256,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -10096,6 +10735,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -10306,6 +10952,141 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -10941,6 +11722,17 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -11341,6 +12133,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -12034,6 +12833,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", @@ -12586,6 +13395,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ollama": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", @@ -13020,6 +13840,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pbf": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", @@ -13929,6 +14756,43 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -14073,6 +14937,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -14264,6 +15138,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -14550,6 +15437,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -14931,6 +15825,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", @@ -14956,6 +15857,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stopword": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/stopword/-/stopword-3.1.5.tgz", @@ -15226,6 +16134,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -15443,6 +16358,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -15472,6 +16394,36 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp-cache": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tmp-cache/-/tmp-cache-1.1.0.tgz", @@ -15520,6 +16472,19 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tqdm": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tqdm/-/tqdm-2.0.3.tgz", @@ -16234,6 +17199,105 @@ "vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -16245,6 +17309,19 @@ "pbf": "^3.2.1" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wasm-feature-detect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", @@ -16322,6 +17399,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", @@ -16453,6 +17547,23 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/admin/package.json b/admin/package.json index fc01737..a9e3a8e 100644 --- a/admin/package.json +++ b/admin/package.json @@ -10,6 +10,7 @@ "build": "node ace build", "dev": "node ace serve --hmr", "test": "node ace test", + "test:unit": "vitest run", "lint": "eslint .", "format": "prettier --write .", "typecheck": "tsc --noEmit", @@ -47,6 +48,8 @@ "@japa/runner": "^4.2.0", "@swc/core": "1.11.24", "@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/luxon": "^3.6.2", "@types/node": "^22.15.18", @@ -55,10 +58,12 @@ "@types/stopword": "^2.0.3", "eslint": "^9.26.0", "hot-hook": "^0.4.0", + "jsdom": "^29.0.1", "prettier": "^3.5.3", "ts-node-maintained": "^10.9.5", "typescript": "~5.8.3", - "vite": "^6.4.1" + "vite": "^6.4.1", + "vitest": "^4.1.0" }, "dependencies": { "@adonisjs/auth": "^9.4.0", diff --git a/admin/tests/unit/utils/fs.test.ts b/admin/tests/unit/utils/fs.test.ts new file mode 100644 index 0000000..57beb1e --- /dev/null +++ b/admin/tests/unit/utils/fs.test.ts @@ -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') + }) +}) diff --git a/admin/tests/unit/utils/misc.test.ts b/admin/tests/unit/utils/misc.test.ts new file mode 100644 index 0000000..9b2c138 --- /dev/null +++ b/admin/tests/unit/utils/misc.test.ts @@ -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) + }) +}) diff --git a/admin/tests/unit/utils/version.test.ts b/admin/tests/unit/utils/version.test.ts new file mode 100644 index 0000000..10ceba9 --- /dev/null +++ b/admin/tests/unit/utils/version.test.ts @@ -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) + }) +}) diff --git a/admin/tests/unit/validators/common.test.ts b/admin/tests/unit/validators/common.test.ts new file mode 100644 index 0000000..9d36282 --- /dev/null +++ b/admin/tests/unit/validators/common.test.ts @@ -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() + }) +}) diff --git a/admin/vitest.config.ts b/admin/vitest.config.ts new file mode 100644 index 0000000..7dba004 --- /dev/null +++ b/admin/vitest.config.ts @@ -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'), + }, + }, +})