From c7a2dea4d1fbe8f18cdf0123db97fdb56dc1a549 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Date: Fri, 16 Aug 2024 22:55:42 +0200 Subject: [PATCH] Feat/frontend vitest (#6469) * Added vitest tests. * Added Settings tests to vitest - not working * Added attributes and attributemap to vitest. * Added more tests. * Also run the vitest tests. * Also run withoutPlugins * Fixed pnpm lock --- .github/workflows/backend-tests.yml | 18 +- pnpm-lock.yaml | 468 ++++++++++++++---- src/node/hooks/express/static.ts | 12 +- src/node/types/PadType.ts | 1 + src/node/utils/{Minify.js => Minify.ts} | 142 +++--- .../{MinifyWorker.js => MinifyWorker.ts} | 10 +- src/node/utils/caching_middleware.ts | 211 -------- src/node/utils/sanitizePathname.ts | 8 +- src/package.json | 8 +- .../specs/AttributeMap.ts} | 10 +- .../specs/attributes.ts} | 38 +- .../backend-new/specs/contentcollector.ts | 398 +++++++++++++++ .../specs/pad_utils.ts | 11 +- .../specs/sanitizePathname.ts | 7 +- src/tests/backend-new/specs/skiplist.ts | 55 ++ src/tests/backend/specs/lowerCasePadIds.ts | 6 +- src/tests/backend/specs/settings.ts | 148 +++--- ...sembler.js => easysync-assembler.spec.mjs} | 27 +- ...ysync-other.js => easysync-other.test.mjs} | 5 +- src/tests/frontend/specs/skiplist.js | 54 -- src/vitest.config.ts | 7 + 21 files changed, 1092 insertions(+), 552 deletions(-) rename src/node/utils/{Minify.js => Minify.ts} (69%) rename src/node/utils/{MinifyWorker.js => MinifyWorker.ts} (83%) delete mode 100644 src/node/utils/caching_middleware.ts rename src/tests/{frontend/specs/AttributeMap.js => backend-new/specs/AttributeMap.ts} (93%) rename src/tests/{frontend/specs/attributes.js => backend-new/specs/attributes.ts} (91%) create mode 100644 src/tests/backend-new/specs/contentcollector.ts rename src/tests/{backend => backend-new}/specs/pad_utils.ts (86%) rename src/tests/{backend => backend-new}/specs/sanitizePathname.ts (92%) create mode 100644 src/tests/backend-new/specs/skiplist.ts rename src/tests/frontend/specs/{easysync-assembler.js => easysync-assembler.spec.mjs} (92%) rename src/tests/frontend/specs/{easysync-other.js => easysync-other.test.mjs} (97%) delete mode 100644 src/tests/frontend/specs/skiplist.js create mode 100644 src/vitest.config.ts diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index fb6c366c9..485cb5eed 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -69,6 +69,9 @@ jobs: - name: Run the backend tests run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsLinux: # run on pushes to any branch @@ -142,6 +145,9 @@ jobs: - name: Run the backend tests run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withoutpluginsWindows: # run on pushes to any branch @@ -193,7 +199,11 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests - run: cd src && pnpm test + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsWindows: # run on pushes to any branch @@ -273,4 +283,8 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests - run: cd src && pnpm test + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0421e26fc..63d8a4794 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,7 +48,7 @@ importers: version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.7.0(vite@5.4.1(@types/node@22.3.0)) + version: 3.7.0(vite@5.4.1(@types/node@22.4.0)) eslint: specifier: ^9.9.0 version: 9.9.0 @@ -90,13 +90,13 @@ importers: version: 5.5.4 vite: specifier: ^5.4.1 - version: 5.4.1(@types/node@22.3.0) + version: 5.4.1(@types/node@22.4.0) vite-plugin-static-copy: specifier: ^1.0.6 - version: 1.0.6(vite@5.4.1(@types/node@22.3.0)) + version: 1.0.6(vite@5.4.1(@types/node@22.4.0)) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(rollup@4.18.0)(typescript@5.5.4)(vite@5.4.1(@types/node@22.3.0)) + version: 4.2.0(rollup@4.18.0)(typescript@5.5.4)(vite@5.4.1(@types/node@22.4.0)) zustand: specifier: ^4.5.5 version: 4.5.5(@types/react@18.3.3)(react@18.3.1) @@ -124,7 +124,7 @@ importers: devDependencies: '@types/node': specifier: ^22.3.0 - version: 22.3.0 + version: 22.4.0 '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -136,7 +136,7 @@ importers: devDependencies: vitepress: specifier: ^1.3.2 - version: 1.3.2(@algolia/client-search@4.23.3)(@types/node@22.3.0)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) + version: 1.3.2(@algolia/client-search@4.23.3)(@types/node@22.4.0)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) src: dependencies: @@ -254,9 +254,6 @@ importers: superagent: specifier: 10.0.2 version: 10.0.2 - threads: - specifier: ^1.7.0 - version: 1.7.0 tinycon: specifier: 0.6.8 version: 0.6.8 @@ -272,6 +269,9 @@ importers: unorm: specifier: 1.6.0 version: 1.6.0 + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@22.4.0)(jsdom@24.1.1) wtfnode: specifier: ^0.9.3 version: 0.9.3 @@ -300,12 +300,15 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.6 version: 9.0.6 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/mocha': specifier: ^10.0.7 version: 10.0.7 '@types/node': specifier: ^22.3.0 - version: 22.3.0 + version: 22.4.0 '@types/oidc-provider': specifier: ^8.5.2 version: 8.5.2 @@ -371,7 +374,7 @@ importers: version: 5.5.4 vite: specifier: ^5.4.1 - version: 5.4.1(@types/node@22.3.0) + version: 5.4.1(@types/node@22.4.0) packages: @@ -1537,6 +1540,9 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1549,8 +1555,8 @@ packages: '@types/node-fetch@2.6.11': resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} - '@types/node@22.3.0': - resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} + '@types/node@22.4.0': + resolution: {integrity: sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==} '@types/oidc-provider@8.5.2': resolution: {integrity: sha512-NiD3VG49+cRCAAe8+uZLM4onOcX8y9+cwaml8JG1qlgc98rWoCRgsnOB4Ypx+ysays5jiwzfUgT0nWyXPB/9uQ==} @@ -1742,6 +1748,24 @@ packages: vite: ^5.0.0 vue: ^3.2.25 + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + '@vue/compiler-core@3.4.31': resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} @@ -1926,6 +1950,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -2006,6 +2034,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cache-content-type@1.0.1: resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} engines: {node: '>= 6.0.0'} @@ -2036,6 +2068,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -2050,6 +2086,10 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2225,6 +2265,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} @@ -2535,10 +2579,6 @@ packages: jiti: optional: true - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - espree@10.1.0: resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2563,6 +2603,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2580,6 +2623,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + express-rate-limit@7.4.0: resolution: {integrity: sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==} engines: {node: '>= 16'} @@ -2744,6 +2791,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2756,6 +2806,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} @@ -2921,6 +2975,10 @@ packages: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + i18next-browser-languagedetector@8.0.0: resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} @@ -3035,10 +3093,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-observable@2.1.0: - resolution: {integrity: sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==} - engines: {node: '>=8'} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -3065,6 +3119,10 @@ packages: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} engines: {node: '>= 0.4'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -3274,6 +3332,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3317,6 +3378,9 @@ packages: merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3362,6 +3426,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -3480,6 +3548,10 @@ packages: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nwsapi@2.2.12: resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} @@ -3515,9 +3587,6 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} - observable-fns@0.6.1: - resolution: {integrity: sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==} - oidc-provider@8.5.1: resolution: {integrity: sha512-Bm3EyxN68/KS76IlciJ3+4pnVtfdRWL+NghWpIF0XQbiRT1gzc6Qf/cyFmpL9yieko/jXYZ/uLHUv77jD00qww==} @@ -3536,6 +3605,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + only@0.0.2: resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} @@ -3602,6 +3675,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3615,6 +3692,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3934,9 +4018,16 @@ packages: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sinon@18.0.0: resolution: {integrity: sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==} @@ -3995,6 +4086,9 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -4003,6 +4097,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamroller@3.1.5: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} @@ -4033,6 +4130,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4097,15 +4198,24 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - threads@1.7.0: - resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} - - tiny-worker@2.3.0: - resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinycon@0.6.8: resolution: {integrity: sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -4204,8 +4314,8 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - undici-types@6.18.2: - resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} + undici-types@6.19.6: + resolution: {integrity: sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4302,6 +4412,11 @@ packages: vfile@6.0.2: resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==} + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-plugin-static-copy@1.0.6: resolution: {integrity: sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4356,6 +4471,31 @@ packages: postcss: optional: true + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -4418,6 +4558,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5468,18 +5613,18 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/async@3.2.24': {} '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/connect@3.4.38': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/content-disposition@0.5.8': {} @@ -5492,11 +5637,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 4.17.21 '@types/keygrip': 1.0.6 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/cors@2.8.17': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/debug@4.1.12': dependencies: @@ -5506,7 +5651,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -5520,11 +5665,11 @@ snapshots: '@types/formidable@3.4.5': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/hast@3.0.4': dependencies: @@ -5542,7 +5687,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 @@ -5552,7 +5697,7 @@ snapshots: '@types/jsonwebtoken@9.0.6': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/keygrip@1.0.6': {} @@ -5569,7 +5714,7 @@ snapshots: '@types/http-errors': 2.0.4 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/linkify-it@5.0.0': {} @@ -5588,6 +5733,8 @@ snapshots: '@types/methods@1.1.4': {} + '@types/mime-types@2.1.4': {} + '@types/mime@1.3.5': {} '@types/mocha@10.0.7': {} @@ -5596,17 +5743,17 @@ snapshots: '@types/node-fetch@2.6.11': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 form-data: 4.0.0 - '@types/node@22.3.0': + '@types/node@22.4.0': dependencies: - undici-types: 6.18.2 + undici-types: 6.19.6 '@types/oidc-provider@8.5.2': dependencies: '@types/koa': 2.15.0 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/prop-types@15.7.12': {} @@ -5628,12 +5775,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/send': 0.17.4 '@types/sinon@17.0.3': @@ -5648,7 +5795,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.3.0 + '@types/node': 22.4.0 '@types/supertest@6.0.2': dependencies: @@ -5657,7 +5804,7 @@ snapshots: '@types/tar@6.1.13': dependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 minipass: 4.2.8 '@types/tough-cookie@4.0.5': {} @@ -5834,18 +5981,51 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.7.0(vite@5.4.1(@types/node@22.3.0))': + '@vitejs/plugin-react-swc@3.7.0(vite@5.4.1(@types/node@22.4.0))': dependencies: '@swc/core': 1.5.28 - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@22.4.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-vue@5.0.5(vite@5.4.1(@types/node@22.3.0))(vue@3.4.31(typescript@5.5.4))': + '@vitejs/plugin-vue@5.0.5(vite@5.4.1(@types/node@22.4.0))(vue@3.4.31(typescript@5.5.4))': dependencies: - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@22.4.0) vue: 3.4.31(typescript@5.5.4) + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.10 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + '@vue/compiler-core@3.4.31': dependencies: '@babel/parser': 7.24.7 @@ -6079,6 +6259,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.6.3 @@ -6164,6 +6346,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cache-content-type@1.0.1: dependencies: mime-types: 2.1.35 @@ -6197,6 +6381,14 @@ snapshots: ccount@2.0.1: {} + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6212,6 +6404,8 @@ snapshots: character-entities-legacy@3.0.0: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -6369,6 +6563,8 @@ snapshots: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-equal@1.0.1: {} deep-is@0.1.4: {} @@ -6467,7 +6663,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 22.3.0 + '@types/node': 22.4.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -6839,9 +7035,6 @@ snapshots: transitivePeerDependencies: - supports-color - esm@3.2.25: - optional: true - espree@10.1.0: dependencies: acorn: 8.12.0 @@ -6862,6 +7055,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + esutils@2.0.3: {} eta@3.4.0: {} @@ -6878,6 +7075,18 @@ snapshots: - supports-color - utf-8-validate + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + express-rate-limit@7.4.0(express@4.19.2): dependencies: express: 4.19.2 @@ -7075,6 +7284,8 @@ snapshots: get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -7087,6 +7298,8 @@ snapshots: get-stream@6.0.1: {} + get-stream@8.0.1: {} + get-symbol-description@1.0.2: dependencies: call-bind: 1.0.7 @@ -7332,6 +7545,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@5.0.0: {} + i18next-browser-languagedetector@8.0.0: dependencies: '@babel/runtime': 7.24.7 @@ -7439,8 +7654,6 @@ snapshots: is-number@7.0.0: {} - is-observable@2.1.0: {} - is-path-inside@3.0.3: {} is-plain-obj@2.1.0: {} @@ -7460,6 +7673,8 @@ snapshots: dependencies: call-bind: 1.0.7 + is-stream@3.0.0: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -7718,6 +7933,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lower-case@2.0.2: dependencies: tslib: 2.6.3 @@ -7763,6 +7982,8 @@ snapshots: merge-descriptors@1.0.1: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -7799,6 +8020,8 @@ snapshots: mime@2.6.0: {} + mimic-fn@4.0.0: {} + mimic-response@3.1.0: {} mimic-response@4.0.0: {} @@ -7912,6 +8135,10 @@ snapshots: normalize-url@8.0.1: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nwsapi@2.2.12: {} object-assign@4.1.1: {} @@ -7948,8 +8175,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - observable-fns@0.6.1: {} - oidc-provider@8.5.1: dependencies: '@koa/cors': 5.0.0 @@ -7980,6 +8205,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + only@0.0.2: {} openapi-backend@5.10.6: @@ -8070,6 +8299,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-to-regexp@0.1.7: {} @@ -8078,6 +8309,10 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.0: {} + perfect-debounce@1.0.0: {} picocolors@1.0.1: {} @@ -8428,8 +8663,12 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sinon@18.0.0: dependencies: '@sinonjs/commons': 3.0.1 @@ -8515,10 +8754,14 @@ snapshots: sprintf-js@1.1.3: {} + stackback@0.0.2: {} + statuses@1.5.0: {} statuses@2.0.1: {} + std-env@3.7.0: {} + streamroller@3.1.5: dependencies: date-format: 4.0.14 @@ -8563,6 +8806,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} superagent@10.0.2: @@ -8654,24 +8899,16 @@ snapshots: text-table@0.2.0: {} - threads@1.7.0: - dependencies: - callsites: 3.1.0 - debug: 4.3.5(supports-color@8.1.1) - is-observable: 2.1.0 - observable-fns: 0.6.1 - optionalDependencies: - tiny-worker: 2.3.0 - transitivePeerDependencies: - - supports-color - - tiny-worker@2.3.0: - dependencies: - esm: 3.2.25 - optional: true + tinybench@2.9.0: {} tinycon@0.6.8: {} + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -8779,7 +9016,7 @@ snapshots: underscore@1.13.7: {} - undici-types@6.18.2: {} + undici-types@6.19.6: {} unified@11.0.5: dependencies: @@ -8880,42 +9117,60 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-plugin-static-copy@1.0.6(vite@5.4.1(@types/node@22.3.0)): + vite-node@2.0.5(@types/node@22.4.0): + dependencies: + cac: 6.7.14 + debug: 4.3.5(supports-color@8.1.1) + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.1(@types/node@22.4.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-static-copy@1.0.6(vite@5.4.1(@types/node@22.4.0)): dependencies: chokidar: 3.6.0 fast-glob: 3.3.2 fs-extra: 11.2.0 picocolors: 1.0.1 - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@22.4.0) - vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.4)(vite@5.4.1(@types/node@22.3.0)): + vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.4)(vite@5.4.1(@types/node@22.4.0)): dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@svgr/core': 8.1.0(typescript@5.5.4) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.5.4)) - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@22.4.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite@5.4.1(@types/node@22.3.0): + vite@5.4.1(@types/node@22.4.0): dependencies: esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.18.0 optionalDependencies: - '@types/node': 22.3.0 + '@types/node': 22.4.0 fsevents: 2.3.3 - vitepress@1.3.2(@algolia/client-search@4.23.3)(@types/node@22.3.0)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4): + vitepress@1.3.2(@algolia/client-search@4.23.3)(@types/node@22.4.0)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4): dependencies: '@docsearch/css': 3.6.0 '@docsearch/js': 3.6.0(@algolia/client-search@4.23.3)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@shikijs/core': 1.10.3 '@shikijs/transformers': 1.10.3 '@types/markdown-it': 14.1.1 - '@vitejs/plugin-vue': 5.0.5(vite@5.4.1(@types/node@22.3.0))(vue@3.4.31(typescript@5.5.4)) + '@vitejs/plugin-vue': 5.0.5(vite@5.4.1(@types/node@22.4.0))(vue@3.4.31(typescript@5.5.4)) '@vue/devtools-api': 7.3.5 '@vue/shared': 3.4.31 '@vueuse/core': 10.11.0(vue@3.4.31(typescript@5.5.4)) @@ -8924,7 +9179,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.0.0 shiki: 1.10.3 - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@22.4.0) vue: 3.4.31(typescript@5.5.4) optionalDependencies: postcss: 8.4.41 @@ -8956,6 +9211,40 @@ snapshots: - typescript - universal-cookie + vitest@2.0.5(@types/node@22.4.0)(jsdom@24.1.1): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.5(supports-color@8.1.1) + execa: 8.0.1 + magic-string: 0.30.10 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.4.1(@types/node@22.4.0) + vite-node: 2.0.5(@types/node@22.4.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.4.0 + jsdom: 24.1.1 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + void-elements@3.1.0: {} vue-demi@0.14.8(vue@3.4.31(typescript@5.5.4)): @@ -9013,6 +9302,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} workerpool@6.5.1: {} diff --git a/src/node/hooks/express/static.ts b/src/node/hooks/express/static.ts index 18ff8c76a..07a7f3a21 100644 --- a/src/node/hooks/express/static.ts +++ b/src/node/hooks/express/static.ts @@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType"; import {PartType} from "../../types/PartType"; const fs = require('fs').promises; -const minify = require('../../utils/Minify'); -const path = require('path'); +import {minify} from '../../utils/Minify'; +import path from 'node:path'; const plugins = require('../../../static/js/pluginfw/plugin_defs'); const settings = require('../../utils/Settings'); -import CachingMiddleware from '../../utils/caching_middleware'; // Rewrite tar to include modules with no extensions and proper rooted paths. const getTar = async () => { @@ -32,15 +31,10 @@ const getTar = async () => { }; exports.expressPreSession = async (hookName:string, {app}:any) => { - // Cache both minified and static. - const assetCache = new CachingMiddleware(); - // Cache static assets - app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache)); - app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache)); // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. - app.all('/static/:filename(*)', minify.minify); + app.all('/static/:filename(*)', minify); // serve plugin definitions // not very static, but served here so that client can do diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index b344ed8c5..5b96ac5f6 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -35,6 +35,7 @@ export type APool = { clone: ()=>APool, check: ()=>Promise, eachAttrib: (callback: (key: string, value: any)=>void)=>void, + getAttrib: (key: number)=>any, } diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.ts similarity index 69% rename from src/node/utils/Minify.js rename to src/node/utils/Minify.ts index bc4bdce67..9660c9098 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.ts @@ -21,20 +21,20 @@ * limitations under the License. */ -const settings = require('./Settings'); -const fs = require('fs').promises; -const path = require('path'); -const plugins = require('../../static/js/pluginfw/plugin_defs'); -const mime = require('mime-types'); -const Threads = require('threads'); -const log4js = require('log4js'); -const sanitizePathname = require('./sanitizePathname'); +import {TransformResult} from "esbuild"; +import mime from 'mime-types'; +import log4js from 'log4js'; +import {compressCSS, compressJS} from './MinifyWorker' +const settings = require('./Settings'); +import {promises as fs} from 'fs'; +import path from 'node:path'; +const plugins = require('../../static/js/pluginfw/plugin_defs'); +import sanitizePathname from './sanitizePathname'; const logger = log4js.getLogger('Minify'); const ROOT_DIR = path.join(settings.root, 'src/static/'); -const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); const LIBRARY_WHITELIST = [ 'async', @@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [ // What follows is a terrible hack to avoid loop-back within the server. // TODO: Serve files from another service, or directly from the file system. -const requestURI = async (url, method, headers) => { +const requestURI = async (url: string | URL, method: any, headers: { [x: string]: any; }) => { const parsedUrl = new URL(url); let status = 500; - const content = []; + const content: any[] = []; const mockRequest = { url, method, @@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => { let mockResponse; const p = new Promise((resolve) => { mockResponse = { - writeHead: (_status, _headers) => { + writeHead: (_status: number, _headers: { [x: string]: any; }) => { status = _status; for (const header in _headers) { if (Object.prototype.hasOwnProperty.call(_headers, header)) { @@ -69,37 +69,63 @@ const requestURI = async (url, method, headers) => { } } }, - setHeader: (header, value) => { + setHeader: (header: string, value: { toString: () => any; }) => { headers[header.toLowerCase()] = value.toString(); }, - header: (header, value) => { + header: (header: string, value: { toString: () => any; }) => { headers[header.toLowerCase()] = value.toString(); }, - write: (_content) => { + write: (_content: any) => { _content && content.push(_content); }, - end: (_content) => { + end: (_content: any) => { _content && content.push(_content); resolve([status, headers, content.join('')]); }, }; }); - await minify(mockRequest, mockResponse); + await _minify(mockRequest, mockResponse); return await p; }; -const requestURIs = (locations, method, headers, callback) => { +const _requestURIs = (locations: any[], method: any, headers: { + [x: string]: + /** + * This Module manages all /minified/* requests. It controls the + * minification && compression of Javascript and CSS. + */ + /* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + any; +}, callback: (arg0: any[], arg1: any[], arg2: any[]) => void) => { Promise.all(locations.map(async (loc) => { try { return await requestURI(loc, method, headers); } catch (err) { logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` + - `${JSON.stringify(headers)}) failed: ${err.stack || err}`); + // @ts-ignore + `${JSON.stringify(headers)}) failed: ${err.stack || err}`); return [500, headers, '']; } })).then((responses) => { + // @ts-ignore const statuss = responses.map((x) => x[0]); + // @ts-ignore const headerss = responses.map((x) => x[1]); + // @ts-ignore const contentss = responses.map((x) => x[2]); callback(statuss, headerss, contentss); }); @@ -119,11 +145,12 @@ const compatPaths = { * @param req the Express request * @param res the Express response */ -const minify = async (req, res) => { +const _minify = async (req:any, res:any) => { let filename = req.params.filename; try { filename = sanitizePathname(filename); } catch (err) { + // @ts-ignore logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`); res.writeHead(404, {}); res.end(); @@ -131,6 +158,7 @@ const minify = async (req, res) => { } // Backward compatibility for plugins that require() files from old paths. + // @ts-ignore const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')]; if (newLocation != null) { logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`); @@ -193,7 +221,7 @@ const minify = async (req, res) => { res.writeHead(200, {}); res.end(); } else if (req.method === 'GET') { - const content = await getFileCompressed(filename, contentType); + const content = await getFileCompressed(filename, contentType as string); res.header('Content-Type', contentType); res.writeHead(200, {}); res.write(content); @@ -205,7 +233,7 @@ const minify = async (req, res) => { }; // Check for the existance of the file and get the last modification date. -const statFile = async (filename, dirStatLimit) => { +const statFile = async (filename: string, dirStatLimit: number):Promise<(any | boolean)[]> => { /* * The only external call to this function provides an explicit value for * dirStatLimit: this check could be removed. @@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => { try { stats = await fs.stat(path.resolve(ROOT_DIR, filename)); } catch (err) { + // @ts-ignore if (['ENOENT', 'ENOTDIR'].includes(err.code)) { // Stat the directory instead. const [date] = await statFile(path.dirname(filename), dirStatLimit - 1); @@ -234,69 +263,64 @@ const statFile = async (filename, dirStatLimit) => { let contentCache = new Map(); -const getFileCompressed = async (filename, contentType) => { +const getFileCompressed = async (filename: any, contentType: string) => { if (contentCache.has(filename)) { return contentCache.get(filename); } - let content = await getFile(filename); + let content: Buffer|string = await getFile(filename); if (!content || !settings.minify) { return content; } else if (contentType === 'application/javascript') { - return await new Promise((resolve) => { - threadsPool.queue(async ({compressJS}) => { + return await new Promise(async (resolve) => { + try { + logger.info('Compress JS file %s.', filename); + + content = content.toString(); try { - logger.info('Compress JS file %s.', filename); - - content = content.toString(); - const compressResult = await compressJS(content); - - if (compressResult.error) { - console.error(`Error compressing JS (${filename}) using terser`, compressResult.error); - } else { - content = compressResult.code.toString(); // Convert content obj code to string - } + let compressResult: TransformResult<{ minify: boolean }> + compressResult = await compressJS(content); + content = compressResult.code.toString(); // Convert content obj code to string } catch (error) { - console.error('getFile() returned an error in ' + - `getFileCompressed(${filename}, ${contentType}): ${error}`); + console.error(`Error compressing JS (${filename}) using esbuild`, error); } - contentCache.set(filename, content); - resolve(content); - }); + } catch (error) { + console.error('getFile() returned an error in ' + + `getFileCompressed(${filename}, ${contentType}): ${error}`); + } + contentCache.set(filename, content); + resolve(content); }); } else if (contentType === 'text/css') { - return await new Promise((resolve) => { - threadsPool.queue(async ({compressCSS}) => { + return await new Promise(async (resolve) => { + try { + logger.info('Compress CSS file %s.', filename); + try { - logger.info('Compress CSS file %s.', filename); - - const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename)); - - if (compressResult.error) { - console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error); - } else { - content = compressResult - } + content = await compressCSS(path.resolve(ROOT_DIR, filename)); } catch (error) { console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`); } contentCache.set(filename, content); resolve(content); - }); - }); + } catch (e) { + console.error('getFile() returned an error in ' + + `getFileCompressed(${filename}, ${contentType}): ${e}`); + } + }) } else { contentCache.set(filename, content); return content; } }; -const getFile = async (filename) => { +const getFile = async (filename: any) => { return await fs.readFile(path.resolve(ROOT_DIR, filename)); }; -exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); +export const minify = (req:any, res:any, next:Function) => _minify(req, res).catch((err) => next(err || new Error(err))); -exports.requestURIs = requestURIs; +export const requestURIs = _requestURIs; -exports.shutdown = async (hookName, context) => { - await threadsPool.terminate(); +export const shutdown = async (hookName: string, context:any) => { + contentCache = new Map(); }; diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.ts similarity index 83% rename from src/node/utils/MinifyWorker.js rename to src/node/utils/MinifyWorker.ts index 507b4f00c..cb278313c 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.ts @@ -3,14 +3,13 @@ * Worker thread to minify JS & CSS files out of the main NodeJS thread */ -import {expose} from 'threads' import {build, transform} from 'esbuild'; /* * Minify JS content * @param {string} content - JS content to minify */ -const compressJS = async (content) => { +export const compressJS = async (content: string) => { return await transform(content, {minify: true}); } @@ -19,7 +18,7 @@ const compressJS = async (content) => { * @param {string} filename - name of the file * @param {string} ROOT_DIR - the root dir of Etherpad */ -const compressCSS = async (content) => { +export const compressCSS = async (content: string) => { const transformedCSS = await build( { entryPoints: [content], @@ -41,8 +40,3 @@ const compressCSS = async (content) => { ) return transformedCSS.outputFiles[0].text }; - -expose({ - compressJS: compressJS, - compressCSS, -}); diff --git a/src/node/utils/caching_middleware.ts b/src/node/utils/caching_middleware.ts deleted file mode 100644 index 74712871c..000000000 --- a/src/node/utils/caching_middleware.ts +++ /dev/null @@ -1,211 +0,0 @@ -'use strict'; - -/* - * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {Buffer} from 'node:buffer' -import fs from 'fs'; -const fsp = fs.promises; -import path from 'path'; -import zlib from 'zlib'; -const settings = require('./Settings'); -const existsSync = require('./path_exists'); -import util from 'util'; - -/* - * The crypto module can be absent on reduced node installations. - * - * Here we copy the approach TypeScript guys used for https://github.com/microsoft/TypeScript/issues/19100 - * If importing crypto fails at runtime, we replace sha256 with djb2, which is - * weaker, but works for our case. - * - * djb2 was written in 1991 by Daniel J. Bernstein. - * - */ - - -import _crypto from 'crypto'; - - -let CACHE_DIR: string|undefined = path.join(settings.root, 'var/'); -CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; - -type Headers = { - [id: string]: string -} - -type ResponseCache = { - [id: string]: { - statusCode: number - headers: Headers - } -} - -const responseCache: ResponseCache = {}; - -const djb2Hash = (data: string) => { - const chars = data.split('').map((str) => str.charCodeAt(0)); - return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; -}; - -const generateCacheKeyWithSha256 = - (path: string) => _crypto.createHash('sha256').update(path).digest('hex'); - -const generateCacheKeyWithDjb2 = - (path: string) => Buffer.from(djb2Hash(path)).toString('hex'); - -let generateCacheKey: (path: string)=>string; - -if (_crypto) { - generateCacheKey = generateCacheKeyWithSha256; -} else { - generateCacheKey = generateCacheKeyWithDjb2; - console.warn('No crypto support in this nodejs runtime. Djb2 (weaker) will be used.'); -} - -// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END - -/* - This caches and compresses 200 and 404 responses to GET and HEAD requests. - TODO: Caching and compressing are solved problems, a middleware configuration - should replace this. -*/ - -export default class CachingMiddleware { - handle(req: any, res: any, next: any) { - this._handle(req, res, next).catch((err) => next(err || new Error(err))); - } - - async _handle(req: any, res: any, next: any) { - if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) { - return next(undefined, req, res); - } - - const oldReq:ResponseCache = {}; - const oldRes:ResponseCache = {}; - - const supportsGzip = - (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; - - const url = new URL(req.url, 'http://localhost'); - const cacheKey = generateCacheKey(url.pathname + url.search); - - const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {}); - const modifiedSince = - req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']); - if (stats != null && stats.mtime && responseCache[cacheKey]) { - req.headers['if-modified-since'] = stats.mtime.toUTCString(); - } else { - delete req.headers['if-modified-since']; - } - - // Always issue get to downstream. - oldReq.method = req.method; - req.method = 'GET'; - - // This handles read/write synchronization as well as its predecessor, - // which is to say, not at all. - // TODO: Implement locking on write or ditch caching of gzip and use - // existing middlewares. - const respond = () => { - req.method = oldReq.method || req.method; - res.write = oldRes.write || res.write; - res.end = oldRes.end || res.end; - - const headers: Headers = {}; - Object.assign(headers, (responseCache[cacheKey].headers || {})); - const statusCode = responseCache[cacheKey].statusCode; - - let pathStr = `${CACHE_DIR}minified_${cacheKey}`; - if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { - pathStr += '.gz'; - headers['content-encoding'] = 'gzip'; - } - - const lastModified = headers['last-modified'] && new Date(headers['last-modified']); - - if (statusCode === 200 && lastModified <= modifiedSince) { - res.writeHead(304, headers); - res.end(); - } else if (req.method === 'GET') { - const readStream = fs.createReadStream(pathStr); - res.writeHead(statusCode, headers); - readStream.pipe(res); - } else { - res.writeHead(statusCode, headers); - res.end(); - } - }; - - const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); - if (expirationDate > new Date()) { - // Our cached version is still valid. - return respond(); - } - - const _headers:Headers = {}; - oldRes.setHeader = res.setHeader; - res.setHeader = (key: string, value: string) => { - // Don't set cookies, see issue #707 - if (key.toLowerCase() === 'set-cookie') return; - - _headers[key.toLowerCase()] = value; - // @ts-ignore - oldRes.setHeader.call(res, key, value); - }; - - oldRes.writeHead = res.writeHead; - res.writeHead = (status: number, headers: Headers) => { - res.writeHead = oldRes.writeHead; - if (status === 200) { - // Update cache - let buffer = ''; - - Object.keys(headers || {}).forEach((key) => { - res.setHeader(key, headers[key]); - }); - headers = _headers; - - oldRes.write = res.write; - oldRes.end = res.end; - res.write = (data: number, encoding: number) => { - buffer += data.toString(encoding); - }; - res.end = async (data: number, encoding: number) => { - await Promise.all([ - fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}), - util.promisify(zlib.gzip)(buffer) - // @ts-ignore - .then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content)) - .catch(() => {}), - ]); - responseCache[cacheKey] = {statusCode: status, headers}; - respond(); - }; - } else if (status === 304) { - // Nothing new changed from the cached version. - oldRes.write = res.write; - oldRes.end = res.end; - res.write = (data: number, encoding: number) => {}; - res.end = (data: number, encoding: number) => { respond(); }; - } else { - res.writeHead(status, headers); - } - }; - - next(undefined, req, res); - } -}; diff --git a/src/node/utils/sanitizePathname.ts b/src/node/utils/sanitizePathname.ts index 2932b913d..72ab88417 100644 --- a/src/node/utils/sanitizePathname.ts +++ b/src/node/utils/sanitizePathname.ts @@ -1,10 +1,8 @@ -'use strict'; - -const path = require('path'); +import path from 'path'; // Normalizes p and ensures that it is a relative path that does not reach outside. See // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. -module.exports = (p: string, pathApi = path) => { +const sanitizeRoot = (p: string, pathApi = path) => { // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., @@ -21,3 +19,5 @@ module.exports = (p: string, pathApi = path) => { if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); return p; }; + +export default sanitizeRoot diff --git a/src/package.json b/src/package.json index 02426efed..4b674201d 100644 --- a/src/package.json +++ b/src/package.json @@ -68,13 +68,13 @@ "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", "superagent": "10.0.2", - "threads": "^1.7.0", "tinycon": "0.6.8", "tsx": "4.17.0", "ueberdb2": "^4.2.93", "underscore": "1.13.7", "unorm": "1.6.0", - "wtfnode": "^0.9.3" + "wtfnode": "^0.9.3", + "vitest": "^2.0.5" }, "bin": { "etherpad-healthcheck": "../bin/etherpad-healthcheck", @@ -89,6 +89,7 @@ "@types/jquery": "^3.5.30", "@types/jsdom": "^21.1.7", "@types/jsonwebtoken": "^9.0.6", + "@types/mime-types": "^2.1.4", "@types/mocha": "^10.0.7", "@types/node": "^22.3.0", "@types/oidc-provider": "^8.5.2", @@ -132,7 +133,8 @@ "test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui", "test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1", "test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1", - "debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts" + "debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts", + "test:vitest": "vitest" }, "version": "2.2.2", "license": "Apache-2.0" diff --git a/src/tests/frontend/specs/AttributeMap.js b/src/tests/backend-new/specs/AttributeMap.ts similarity index 93% rename from src/tests/frontend/specs/AttributeMap.js rename to src/tests/backend-new/specs/AttributeMap.ts index 92ca68334..3e43abf7c 100644 --- a/src/tests/frontend/specs/AttributeMap.js +++ b/src/tests/backend-new/specs/AttributeMap.ts @@ -1,8 +1,9 @@ 'use strict'; -const AttributeMap = require('../../../static/js/AttributeMap'); +const AttributeMap = require('../../../static/js/AttributeMap.js'); const AttributePool = require('../../../static/js/AttributePool'); const attributes = require('../../../static/js/attributes'); +import {expect, describe, it, beforeEach} from 'vitest' describe('AttributeMap', function () { const attribs = [ @@ -10,7 +11,7 @@ describe('AttributeMap', function () { ['baz', 'bif'], ['emptyValue', ''], ]; - let pool; + let pool: { eachAttrib: (arg0: () => number) => void; putAttrib: (arg0: string[]) => any; getAttrib: (arg0: number) => any; }; const getPoolSize = () => { let n = 0; @@ -66,7 +67,7 @@ describe('AttributeMap', function () { ['number', 1, '1'], ]; for (const [desc, input, want] of testCases) { - describe(desc, function () { + describe(desc as string, function () { it('key is coerced to string', async function () { const m = new AttributeMap(pool); m.set(input, 'value'); @@ -116,8 +117,9 @@ describe('AttributeMap', function () { }); for (const funcName of ['update', 'updateFromString']) { - const callUpdateFn = (m, ...args) => { + const callUpdateFn = (m: any, ...args: (boolean | (string | null | undefined)[][])[]) => { if (funcName === 'updateFromString') { + // @ts-ignore args[0] = attributes.attribsToString(attributes.sort([...args[0]]), pool); } return AttributeMap.prototype[funcName].call(m, ...args); diff --git a/src/tests/frontend/specs/attributes.js b/src/tests/backend-new/specs/attributes.ts similarity index 91% rename from src/tests/frontend/specs/attributes.js rename to src/tests/backend-new/specs/attributes.ts index 13058dbe3..5f33f52b3 100644 --- a/src/tests/frontend/specs/attributes.js +++ b/src/tests/backend-new/specs/attributes.ts @@ -1,11 +1,15 @@ 'use strict'; +import {APool} from "../../../node/types/PadType"; + const AttributePool = require('../../../static/js/AttributePool'); const attributes = require('../../../static/js/attributes'); +import {expect, describe, it, beforeEach} from 'vitest'; + describe('attributes', function () { const attribs = [['foo', 'bar'], ['baz', 'bif']]; - let pool; + let pool: APool; beforeEach(async function () { pool = new AttributePool(); @@ -14,7 +18,7 @@ describe('attributes', function () { describe('decodeAttribString', function () { it('is a generator function', async function () { - expect(attributes.decodeAttribString).to.be.a((function* () {}).constructor); + expect(attributes.decodeAttribString.constructor.name).to.equal('GeneratorFunction'); }); describe('rejects invalid attribute strings', function () { @@ -22,7 +26,7 @@ describe('attributes', function () { for (const tc of testCases) { it(JSON.stringify(tc), async function () { expect(() => [...attributes.decodeAttribString(tc)]) - .to.throwException(/invalid character/); + .toThrowError(/invalid character/); }); } }); @@ -56,7 +60,7 @@ describe('attributes', function () { ['set', new Set([0, 1])], ]; for (const [desc, input] of testCases) { - it(desc, async function () { + it(desc as string, async function () { expect(attributes.encodeAttribString(input)).to.equal('*0*1'); }); } @@ -74,7 +78,7 @@ describe('attributes', function () { ]; for (const [input, wantErr] of testCases) { it(JSON.stringify(input), async function () { - expect(() => attributes.encodeAttribString(input)).to.throwException(wantErr); + expect(() => attributes.encodeAttribString(input)).toThrowError(wantErr as RegExp); }); } }); @@ -101,7 +105,7 @@ describe('attributes', function () { describe('attribsFromNums', function () { it('is a generator function', async function () { - expect(attributes.attribsFromNums).to.be.a((function* () {}).constructor); + expect(attributes.attribsFromNums.constructor.name).to.equal("GeneratorFunction"); }); describe('accepts any kind of iterable', function () { @@ -112,7 +116,7 @@ describe('attributes', function () { ]; for (const [desc, input] of testCases) { - it(desc, async function () { + it(desc as string, async function () { const gotAttribs = [...attributes.attribsFromNums(input, pool)]; expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(attribs)); }); @@ -132,7 +136,7 @@ describe('attributes', function () { ]; for (const [input, wantErr] of testCases) { it(JSON.stringify(input), async function () { - expect(() => [...attributes.attribsFromNums(input, pool)]).to.throwException(wantErr); + expect(() => [...attributes.attribsFromNums(input, pool)]).toThrowError(wantErr as RegExp); }); } }); @@ -156,7 +160,7 @@ describe('attributes', function () { describe('attribsToNums', function () { it('is a generator function', async function () { - expect(attributes.attribsToNums).to.be.a((function* () {}).constructor); + expect(attributes.attribsToNums.constructor.name).to.equal("GeneratorFunction") }); describe('accepts any kind of iterable', function () { @@ -167,7 +171,7 @@ describe('attributes', function () { ]; for (const [desc, input] of testCases) { - it(desc, async function () { + it(desc as string, async function () { const gotNums = [...attributes.attribsToNums(input, pool)]; expect(JSON.stringify(gotNums)).to.equal(JSON.stringify([0, 1])); }); @@ -178,7 +182,7 @@ describe('attributes', function () { const testCases = [null, [null]]; for (const input of testCases) { it(JSON.stringify(input), async function () { - expect(() => [...attributes.attribsToNums(input, pool)]).to.throwException(); + expect(() => [...attributes.attribsToNums(input, pool)]).toThrowError(); }); } }); @@ -224,12 +228,12 @@ describe('attributes', function () { ['number', 1, '1'], ]; for (const [desc, inputVal, wantVal] of testCases) { - describe(desc, function () { + describe(desc as string, function () { for (const [desc, inputAttribs, wantAttribs] of [ ['key is coerced to string', [[inputVal, 'value']], [[wantVal, 'value']]], ['value is coerced to string', [['key', inputVal]], [['key', wantVal]]], ]) { - it(desc, async function () { + it(desc as string, async function () { const gotNums = [...attributes.attribsToNums(inputAttribs, pool)]; // Each attrib in inputAttribs is expected to be new to the pool. const wantNums = [...Array(attribs.length + 1).keys()].slice(attribs.length); @@ -245,7 +249,7 @@ describe('attributes', function () { describe('attribsFromString', function () { it('is a generator function', async function () { - expect(attributes.attribsFromString).to.be.a((function* () {}).constructor); + expect(attributes.attribsFromString.constructor.name).to.equal('GeneratorFunction'); }); describe('rejects invalid attribute strings', function () { @@ -261,7 +265,7 @@ describe('attributes', function () { ]; for (const [input, wantErr] of testCases) { it(JSON.stringify(input), async function () { - expect(() => [...attributes.attribsFromString(input, pool)]).to.throwException(wantErr); + expect(() => [...attributes.attribsFromString(input, pool)]).toThrowError(wantErr); }); } }); @@ -292,7 +296,7 @@ describe('attributes', function () { ]; for (const [desc, input] of testCases) { - it(desc, async function () { + it(desc as string, async function () { const got = attributes.attribsToString(input, pool); expect(got).to.equal('*0*1'); }); @@ -303,7 +307,7 @@ describe('attributes', function () { const testCases = [null, [null]]; for (const input of testCases) { it(JSON.stringify(input), async function () { - expect(() => attributes.attribsToString(input, pool)).to.throwException(); + expect(() => attributes.attribsToString(input, pool)).toThrowError(); }); } }); diff --git a/src/tests/backend-new/specs/contentcollector.ts b/src/tests/backend-new/specs/contentcollector.ts new file mode 100644 index 000000000..81f0bc2ea --- /dev/null +++ b/src/tests/backend-new/specs/contentcollector.ts @@ -0,0 +1,398 @@ +'use strict'; + +/* + * While importexport tests target the `setHTML` API endpoint, which is nearly identical to what + * happens when a user manually imports a document via the UI, the contentcollector tests here don't + * use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the + * expected results here can differ from importexport.js. + * + * If you add tests here, please also add them to importexport.js + */ + +import {APool} from "../../../node/types/PadType"; + +const AttributePool = require('../../../static/js/AttributePool'); +const Changeset = require('../../../static/js/Changeset'); +const assert = require('assert').strict; +const attributes = require('../../../static/js/attributes'); +const contentcollector = require('../../../static/js/contentcollector'); +const jsdom = require('jsdom'); + +import {describe, it, beforeAll, test} from 'vitest'; + +// All test case `wantAlines` values must only refer to attributes in this list so that the +// attribute numbers do not change due to changes in pool insertion order. +const knownAttribs = [ + ['insertorder', 'first'], + ['italic', 'true'], + ['list', 'bullet1'], + ['list', 'bullet2'], + ['list', 'number1'], + ['list', 'number2'], + ['lmkr', '1'], + ['start', '1'], + ['start', '2'], +]; + +const testCases = [ + { + description: 'Simple', + html: '

foo

', + wantAlines: ['+3'], + wantText: ['foo'], + }, + { + description: 'Line starts with asterisk', + html: '

*foo

', + wantAlines: ['+4'], + wantText: ['*foo'], + }, + { + description: 'Complex nested Li', + html: '
  1. one
    1. 1.1
  2. two
', + wantAlines: [ + '*0*4*6*7+1+3', + '*0*5*6*8+1+3', + '*0*4*6*8+1+3', + ], + wantText: [ + '*one', '*1.1', '*two', + ], + }, + { + description: 'Complex list of different types', + html: '
  1. item
    1. item1
    2. item2
', + wantAlines: [ + '*0*2*6+1+3', + '*0*2*6+1+3', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*3*6+1+1', + '*0*4*6*7+1+4', + '*0*5*6*8+1+5', + '*0*5*6*8+1+5', + ], + wantText: [ + '*one', + '*two', + '*0', + '*1', + '*2', + '*3', + '*4', + '*item', + '*item1', + '*item2', + ], + }, + { + description: 'Tests if uls properly get attributes', + html: '
div

foo

', + wantAlines: [ + '*0*2*6+1+1', + '*0*2*6+1+1', + '+3', + '+3', + ], + wantText: ['*a', '*b', 'div', 'foo'], + }, + { + description: 'Tests if indented uls properly get attributes', + html: '

foo

', + wantAlines: [ + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*2*6+1+1', + '+3', + ], + wantText: ['*a', '*b', '*a', 'foo'], + }, + { + description: 'Tests if ols properly get line numbers when in a normal OL', + html: '
  1. a
  2. b
  3. c

test

', + wantAlines: [ + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '+4', + ], + wantText: ['*a', '*b', '*c', 'test'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + }, + { + description: 'A single completely empty line break within an ol should reset count if OL is closed off..', + html: '
  1. should be 1

hello

  1. should be 1
  2. should be 2

', + wantAlines: [ + '*0*4*6*7+1+b', + '+5', + '*0*4*6*8+1+b', + '*0*4*6*8+1+b', + '', + ], + wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''], + noteToSelf: "Shouldn't include attribute marker in the

line", + }, + { + description: 'A single

should create a new line', + html: '

', + wantAlines: ['', ''], + wantText: ['', ''], + noteToSelf: '

should create a line break but not break numbering', + }, + { + description: 'Tests if ols properly get line numbers when in a normal OL #2', + html: 'a
  1. b
    1. c
notlist

foo

', + wantAlines: [ + '+1', + '*0*4*6*7+1+1', + '*0*5*6*8+1+1', + '+7', + '+3', + ], + wantText: ['a', '*b', '*c', 'notlist', 'foo'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + }, + { + description: 'First item being an UL then subsequent being OL will fail', + html: '', + wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'], + wantText: ['a', '*b', '*c'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + disabled: true, + }, + { + description: 'A single completely empty line break within an ol should NOT reset count', + html: '
  1. should be 1
  2. should be 2
  3. should be 3

', + wantAlines: [], + wantText: ['*should be 1', '*should be 2', '*should be 3'], + noteToSelf: "

should create a line break but not break numbering -- This is what I can't get working!", + disabled: true, + }, + { + description: 'Content outside body should be ignored', + html: 'titleempty
', + wantAlines: ['+5'], + wantText: ['empty'], + }, + { + description: 'Multiple spaces should be preserved', + html: 'Text with more than one space.
', + wantAlines: ['+10'], + wantText: ['Text with more than one space.'], + }, + { + description: 'non-breaking and normal space should be preserved', + html: 'Text with  more   than  one space.
', + wantAlines: ['+10'], + wantText: ['Text with more than one space.'], + }, + { + description: 'Multiple nbsp should be preserved', + html: '  
', + wantAlines: ['+2'], + wantText: [' '], + }, + { + description: 'Multiple nbsp between words ', + html: '  word1  word2   word3
', + wantAlines: ['+m'], + wantText: [' word1 word2 word3'], + }, + { + description: 'A non-breaking space preceded by a normal space', + html: '  word1  word2  word3
', + wantAlines: ['+l'], + wantText: [' word1 word2 word3'], + }, + { + description: 'A non-breaking space followed by a normal space', + html: '  word1  word2  word3
', + wantAlines: ['+l'], + wantText: [' word1 word2 word3'], + }, + { + description: 'Don\'t collapse spaces that follow a newline', + html: 'something
something
', + wantAlines: ['+9', '+m'], + wantText: ['something', ' something'], + }, + { + description: 'Don\'t collapse spaces that follow a empty paragraph', + html: 'something

something
', + wantAlines: ['+9', '', '+m'], + wantText: ['something', '', ' something'], + }, + { + description: 'Don\'t collapse spaces that preceed/follow a newline', + html: 'something
something
', + wantAlines: ['+l', '+m'], + wantText: ['something ', ' something'], + }, + { + description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', + html: 'something

something
', + wantAlines: ['+l', '', '+m'], + wantText: ['something ', '', ' something'], + }, + { + description: 'Don\'t collapse non-breaking spaces that follow a newline', + html: 'something
   something
', + wantAlines: ['+9', '+c'], + wantText: ['something', ' something'], + }, + { + description: 'Don\'t collapse non-breaking spaces that follow a paragraph', + html: 'something

   something
', + wantAlines: ['+9', '', '+c'], + wantText: ['something', '', ' something'], + }, + { + description: 'Preserve all spaces when multiple are present', + html: 'Need more space s !
', + wantAlines: ['+h*1+4+2'], + wantText: ['Need more space s !'], + }, + { + description: 'Newlines and multiple spaces across newlines should be preserved', + html: ` + Need + more + space + s + !
`, + wantAlines: ['+19*1+4+b'], + wantText: ['Need more space s !'], + }, + { + description: 'Multiple new lines at the beginning should be preserved', + html: '

first line

second line
', + wantAlines: ['', '', '', '', '+a', '', '+b'], + wantText: ['', '', '', '', 'first line', '', 'second line'], + }, + { + description: 'A paragraph with multiple lines should not loose spaces when lines are combined', + html: `

+а б в г ґ д е є ж з и і ї й к л м н о +п р с т у ф х ц ч ш щ ю я ь

+`, + wantAlines: ['+1t'], + wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'], + }, + { + description: 'lines in preformatted text should be kept intact', + html: `

+а б в г ґ д е є ж з и і ї й к л м н о

multiple
+lines
+in
+pre
+

п р с т у ф х ц ч ш щ ю я +ь

+`, + wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'], + wantText: [ + 'а б в г ґ д е є ж з и і ї й к л м н о', + 'multiple', + 'lines', + 'in', + 'pre', + 'п р с т у ф х ц ч ш щ ю я ь', + ], + }, + { + description: 'pre should be on a new line not preceded by a space', + html: `

+ 1 +

preline
+
`, + wantAlines: ['+6', '+7'], + wantText: [' 1 ', 'preline'], + }, + { + description: 'Preserve spaces on the beginning and end of a element', + html: 'Need more space s !
', + wantAlines: ['+f*1+3+1'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces outside elements', + html: 'Need more space s !
', + wantAlines: ['+g*1+1+2'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces at the end of an element', + html: 'Need more space s !
', + wantAlines: ['+g*1+2+1'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces at the start of an element', + html: 'Need more space s !
', + wantAlines: ['+f*1+2+2'], + wantText: ['Need more space s !'], + }, +]; + +describe(__filename, function () { + for (const tc of testCases) { + describe(tc.description, function () { + let apool: APool; + let result: { + lines: string[], + lineAttribs: string[], + }; + if (tc.disabled) { + test.skip('If disabled we do not run the test'); + return; + } + + beforeAll(async function () { + + const {window: {document}} = new jsdom.JSDOM(tc.html); + apool = new AttributePool(); + // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all + // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute + // numbers do not change if the attribute processing code changes.) + for (const attrib of knownAttribs) apool.putAttrib(attrib); + for (const aline of tc.wantAlines) { + for (const op of Changeset.deserializeOps(aline)) { + for (const n of attributes.decodeAttribString(op.attribs)) { + assert(n < knownAttribs.length); + } + } + } + const cc = contentcollector.makeContentCollector(true, null, apool); + cc.collectContent(document.body); + result = cc.finish(); + console.log(result); + }); + + it('text matches', async function () { + assert.deepEqual(result.lines, tc.wantText); + }); + + it('alines match', async function () { + assert.deepEqual(result.lineAttribs, tc.wantAlines); + }); + + it('attributes are sorted in canonical order', async function () { + const gotAttribs:string[][][] = []; + const wantAttribs = []; + for (const aline of result.lineAttribs) { + const gotAlineAttribs:string[][] = []; + gotAttribs.push(gotAlineAttribs); + const wantAlineAttribs:string[] = []; + wantAttribs.push(wantAlineAttribs); + for (const op of Changeset.deserializeOps(aline)) { + const gotOpAttribs:string[] = [...attributes.attribsFromString(op.attribs, apool)]; + gotAlineAttribs.push(gotOpAttribs); + wantAlineAttribs.push(attributes.sort([...gotOpAttribs])); + } + } + assert.deepEqual(gotAttribs, wantAttribs); + }); + }); + } +}); diff --git a/src/tests/backend/specs/pad_utils.ts b/src/tests/backend-new/specs/pad_utils.ts similarity index 86% rename from src/tests/backend/specs/pad_utils.ts rename to src/tests/backend-new/specs/pad_utils.ts index 3ca3c0858..bcccdf449 100644 --- a/src/tests/backend/specs/pad_utils.ts +++ b/src/tests/backend-new/specs/pad_utils.ts @@ -1,16 +1,13 @@ -'use strict'; - import {MapArrayType} from "../../../node/types/MapType"; - -import {strict as assert} from "assert"; const {padutils} = require('../../../static/js/pad_utils'); +import {describe, it, expect, afterEach, beforeAll} from "vitest"; describe(__filename, function () { describe('warnDeprecated', function () { const {warnDeprecated} = padutils; const backups:MapArrayType = {}; - before(async function () { + beforeAll(async function () { backups.logger = warnDeprecated.logger; }); @@ -36,10 +33,10 @@ describe(__filename, function () { for (const [now, want] of testCases) { // In a loop so that the stack trace is the same. warnDeprecated._rl.now = () => now; warnDeprecated(); - assert.equal(got, want); + expect(got).toEqual(want); } warnDeprecated(); // Should have a different stack trace. - assert.equal(got, testCases[testCases.length - 1][1] + 1); + expect(got).toEqual(testCases[testCases.length - 1][1] + 1); }); }); }); diff --git a/src/tests/backend/specs/sanitizePathname.ts b/src/tests/backend-new/specs/sanitizePathname.ts similarity index 92% rename from src/tests/backend/specs/sanitizePathname.ts rename to src/tests/backend-new/specs/sanitizePathname.ts index fd3cbb2e7..e841ae155 100644 --- a/src/tests/backend/specs/sanitizePathname.ts +++ b/src/tests/backend-new/specs/sanitizePathname.ts @@ -1,8 +1,7 @@ -'use strict'; - import {strict as assert} from "assert"; import path from 'path'; -const sanitizePathname = require('../../../node/utils/sanitizePathname'); +import sanitizePathname from '../../../node/utils/sanitizePathname'; +import {describe, it, expect} from 'vitest'; describe(__filename, function () { describe('absolute paths rejected', function () { @@ -21,7 +20,7 @@ describe(__filename, function () { for (const [platform, p] of testCases) { it(`${platform} ${p}`, async function () { // @ts-ignore - assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/}); + expect(() => sanitizePathname(p, path[platform] as any)).toThrowError(/absolute path/); }); } }); diff --git a/src/tests/backend-new/specs/skiplist.ts b/src/tests/backend-new/specs/skiplist.ts new file mode 100644 index 000000000..c1b408e3a --- /dev/null +++ b/src/tests/backend-new/specs/skiplist.ts @@ -0,0 +1,55 @@ +'use strict'; + +const SkipList = require('ep_etherpad-lite/static/js/skiplist'); +import {expect, describe, it} from 'vitest'; + +describe('skiplist.js', function () { + it('rejects null keys', async function () { + const skiplist = new SkipList(); + for (const key of [undefined, null]) { + expect(() => skiplist.push({key})).toThrowError(); + } + }); + + it('rejects duplicate keys', async function () { + const skiplist = new SkipList(); + skiplist.push({key: 'foo'}); + expect(() => skiplist.push({key: 'foo'})).toThrowError(); + }); + + it('atOffset() returns last entry that touches offset', async function () { + const skiplist = new SkipList(); + const entries: { key: string; width: number; }[] = []; + let nextId = 0; + const makeEntry = (width: number) => { + const entry = {key: `id${nextId++}`, width}; + entries.push(entry); + return entry; + }; + + skiplist.push(makeEntry(5)); + expect(skiplist.atOffset(4)).toBe(entries[0]); + expect(skiplist.atOffset(5)).toBe(entries[0]); + expect(() => skiplist.atOffset(6)).toThrowError(); + + skiplist.push(makeEntry(0)); + expect(skiplist.atOffset(4)).toBe(entries[0]); + expect(skiplist.atOffset(5)).toBe(entries[1]); + expect(() => skiplist.atOffset(6)).toThrowError(); + + skiplist.push(makeEntry(0)); + expect(skiplist.atOffset(4)).toBe(entries[0]); + expect(skiplist.atOffset(5)).toBe(entries[2]); + expect(() => skiplist.atOffset(6)).toThrowError(); + + skiplist.splice(2, 0, [makeEntry(0)]); + expect(skiplist.atOffset(4)).toBe(entries[0]); + expect(skiplist.atOffset(5)).toBe(entries[2]); + expect(() => skiplist.atOffset(6)).toThrowError(); + + skiplist.push(makeEntry(3)); + expect(skiplist.atOffset(4)).toBe(entries[0]); + expect(skiplist.atOffset(5)).toBe(entries[4]); + expect(skiplist.atOffset(6)).toBe(entries[4]); + }); +}); diff --git a/src/tests/backend/specs/lowerCasePadIds.ts b/src/tests/backend/specs/lowerCasePadIds.ts index 359f85d2c..c85d16c3f 100644 --- a/src/tests/backend/specs/lowerCasePadIds.ts +++ b/src/tests/backend/specs/lowerCasePadIds.ts @@ -40,7 +40,7 @@ describe(__filename, function () { it('do nothing', async function () { await agent.get('/p/UPPERCASEpad') - .expect(200); + .expect(200); }); }); @@ -50,8 +50,8 @@ describe(__filename, function () { }); it('lowercase pad ids', async function () { await agent.get('/p/UPPERCASEpad') - .expect(302) - .expect('location', 'uppercasepad'); + .expect(302) + .expect('location', 'uppercasepad'); }); it('keeps old pads accessible', async function () { diff --git a/src/tests/backend/specs/settings.ts b/src/tests/backend/specs/settings.ts index d9bfe4f6d..d6dcaf71a 100644 --- a/src/tests/backend/specs/settings.ts +++ b/src/tests/backend/specs/settings.ts @@ -6,87 +6,87 @@ import path from 'path'; import process from 'process'; describe(__filename, function () { - describe('parseSettings', function () { - let settings: any; - const envVarSubstTestCases = [ - {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, - {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, - {name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null}, - {name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined}, - {name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123}, - {name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'}, - {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''}, - ]; + describe('parseSettings', function () { + let settings: any; + const envVarSubstTestCases = [ + {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, + {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, + {name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null}, + {name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined}, + {name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123}, + {name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'}, + {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''}, + ]; - before(async function () { - for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; - delete process.env.UNSET_VAR; - settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert(settings != null); - }); - - describe('environment variable substitution', function () { - describe('set', function () { - for (const tc of envVarSubstTestCases) { - it(tc.name, async function () { - const obj = settings['environment variable substitution'].set; - if (tc.name === 'undefined') { - assert(!(tc.name in obj)); - } else { - assert.equal(obj[tc.name], tc.want); - } - }); - } - }); - - describe('unset', function () { - it('no default', async function () { - const obj = settings['environment variable substitution'].unset; - assert.equal(obj['no default'], null); - }); - - for (const tc of envVarSubstTestCases) { - it(tc.name, async function () { - const obj = settings['environment variable substitution'].unset; - if (tc.name === 'undefined') { - assert(!(tc.name in obj)); - } else { - assert.equal(obj[tc.name], tc.want); - } - }); - } - }); - }); + before(async function () { + for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; + delete process.env.UNSET_VAR; + settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert(settings != null); }); + describe('environment variable substitution', function () { + describe('set', function () { + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].set; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); - describe("Parse plugin settings", function () { + describe('unset', function () { + it('no default', async function () { + const obj = settings['environment variable substitution'].unset; + assert.equal(obj['no default'], null); + }); - before(async function () { - process.env["EP__ADMIN__PASSWORD"] = "test" - }) + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].unset; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); + }); + }); - it('should parse plugin settings', async function () { - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.equal(settings.ADMIN.PASSWORD, "test"); - }) - it('should bundle settings with same path', async function () { - process.env["EP__ADMIN__USERNAME"] = "test" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"}); - }) + describe("Parse plugin settings", function () { - it("Can set the ep themes", async function () { - process.env["EP__ep_themes__default_theme"] = "hacker" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"}); - }) - - it("can set the ep_webrtc settings", async function () { - process.env["EP__ep_webrtc__enabled"] = "true" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ep_webrtc, {"enabled": true}); - }) + before(async function () { + process.env["EP__ADMIN__PASSWORD"] = "test" }) + + it('should parse plugin settings', async function () { + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.equal(settings.ADMIN.PASSWORD, "test"); + }) + + it('should bundle settings with same path', async function () { + process.env["EP__ADMIN__USERNAME"] = "test" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"}); + }) + + it("Can set the ep themes", async function () { + process.env["EP__ep_themes__default_theme"] = "hacker" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"}); + }) + + it("can set the ep_webrtc settings", async function () { + process.env["EP__ep_webrtc__enabled"] = "true" + let settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings.ep_webrtc, {"enabled": true}); + }) + }) }); diff --git a/src/tests/frontend/specs/easysync-assembler.js b/src/tests/frontend/specs/easysync-assembler.spec.mjs similarity index 92% rename from src/tests/frontend/specs/easysync-assembler.js rename to src/tests/frontend/specs/easysync-assembler.spec.mjs index 80a6636d2..ffccfc7e3 100644 --- a/src/tests/frontend/specs/easysync-assembler.js +++ b/src/tests/frontend/specs/easysync-assembler.spec.mjs @@ -4,11 +4,19 @@ const Changeset = require('../../../static/js/Changeset'); const {padutils} = require('../../../static/js/pad_utils'); const {poolOrArray} = require('../easysync-helper.js'); +import {describe, it, expect} from 'vitest' + + describe('easysync-assembler', function () { it('opAssembler', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; const assem = Changeset.opAssembler(); - for (const op of Changeset.deserializeOps(x)) assem.append(op); + var opLength = 0 + for (const op of Changeset.deserializeOps(x)){ + console.log(op) + assem.append(op); + opLength++ + } expect(assem.toString()).to.equal(x); }); @@ -145,14 +153,23 @@ describe('easysync-assembler', function () { next() { const v = this._n.value; this._n = ops.next(); return v; }, }; const assem = Changeset.smartOpAssembler(); - assem.append(iter.next()); - assem.append(iter.next()); - assem.append(iter.next()); + var iter1 = iter.next() + assem.append(iter1); + var iter2 = iter.next() + assem.append(iter2); + var iter3 = iter.next() + assem.append(iter3); + console.log(assem.toString()); assem.clear(); assem.append(iter.next()); assem.append(iter.next()); + console.log(assem.toString()); assem.clear(); - while (iter.hasNext()) assem.append(iter.next()); + let counter = 0; + while (iter.hasNext()) { + console.log(counter++) + assem.append(iter.next()); + } assem.endDocument(); expect(assem.toString()).to.equal('-1+1*0+1=1-1+1|c=c-1'); }); diff --git a/src/tests/frontend/specs/easysync-other.js b/src/tests/frontend/specs/easysync-other.test.mjs similarity index 97% rename from src/tests/frontend/specs/easysync-other.js rename to src/tests/frontend/specs/easysync-other.test.mjs index af4580835..3c0bdff7c 100644 --- a/src/tests/frontend/specs/easysync-other.js +++ b/src/tests/frontend/specs/easysync-other.test.mjs @@ -4,6 +4,8 @@ const Changeset = require('../../../static/js/Changeset'); const AttributePool = require('../../../static/js/AttributePool'); const {randomMultiline, poolOrArray} = require('../easysync-helper.js'); const {padutils} = require('../../../static/js/pad_utils'); +import {describe, it, expect} from 'vitest' + describe('easysync-other', function () { describe('filter attribute numbers', function () { @@ -66,7 +68,8 @@ describe('easysync-other', function () { it('testMakeSplice', async function () { const t = 'a\nb\nc\n'; - const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t); + let splice = Changeset.makeSplice(t, 5, 0, 'def') + const t2 = Changeset.applyToText(splice, t); expect(t2).to.equal('a\nb\ncdef\n'); }); diff --git a/src/tests/frontend/specs/skiplist.js b/src/tests/frontend/specs/skiplist.js deleted file mode 100644 index 16b985615..000000000 --- a/src/tests/frontend/specs/skiplist.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const SkipList = require('ep_etherpad-lite/static/js/skiplist'); - -describe('skiplist.js', function () { - it('rejects null keys', async function () { - const skiplist = new SkipList(); - for (const key of [undefined, null]) { - expect(() => skiplist.push({key})).to.throwError(); - } - }); - - it('rejects duplicate keys', async function () { - const skiplist = new SkipList(); - skiplist.push({key: 'foo'}); - expect(() => skiplist.push({key: 'foo'})).to.throwError(); - }); - - it('atOffset() returns last entry that touches offset', async function () { - const skiplist = new SkipList(); - const entries = []; - let nextId = 0; - const makeEntry = (width) => { - const entry = {key: `id${nextId++}`, width}; - entries.push(entry); - return entry; - }; - - skiplist.push(makeEntry(5)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[0]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(0)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[1]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(0)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[2]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.splice(2, 0, [makeEntry(0)]); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[2]); - expect(() => skiplist.atOffset(6)).to.throwError(); - - skiplist.push(makeEntry(3)); - expect(skiplist.atOffset(4)).to.be(entries[0]); - expect(skiplist.atOffset(5)).to.be(entries[4]); - expect(skiplist.atOffset(6)).to.be(entries[4]); - }); -}); diff --git a/src/vitest.config.ts b/src/vitest.config.ts new file mode 100644 index 000000000..945b62cf0 --- /dev/null +++ b/src/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ["tests/backend-new/**/*.ts"], + }, +})