diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0ace07ab..05ea051e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -66,3 +66,31 @@ jobs: cd src pnpm install pnpm ${{ matrix.command }} + + test: + name: Test + runs-on: ubuntu-latest + needs: docs + if: github.repository_owner == 'wg-easy' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "lts/jod" + check-latest: true + cache: "pnpm" + + - name: Run tests + run: | + cd src + pnpm install + pnpm test:run diff --git a/src/package.json b/src/package.json index 89bef3f9..f5d8cf37 100644 --- a/src/package.json +++ b/src/package.json @@ -15,6 +15,9 @@ "format:check": "prettier . --check", "typecheck": "nuxt typecheck", "check:all": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", "db:generate": "drizzle-kit generate", "cli:build": "node cli/build.js", "cli:dev": "tsx cli/index.ts" @@ -59,6 +62,7 @@ "@types/debug": "^4.1.12", "@types/phc__format": "^1.0.1", "@types/semver": "^7.7.1", + "@vitest/ui": "^2.1.0", "drizzle-kit": "^0.31.6", "esbuild": "^0.25.12", "eslint": "^9.39.0", @@ -67,6 +71,7 @@ "prettier-plugin-tailwindcss": "^0.7.1", "tsx": "^4.20.6", "typescript": "^5.9.3", + "vitest": "^2.1.0", "vue-tsc": "^3.1.3" }, "packageManager": "pnpm@10.20.0", diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index 6a6edda4..4e2073d1 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: '@types/semver': specifier: ^7.7.1 version: 7.7.1 + '@vitest/ui': + specifier: ^2.1.0 + version: 2.1.9(vitest@2.1.9) drizzle-kit: specifier: ^0.31.6 version: 0.31.6 @@ -147,6 +150,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@24.10.0)(@vitest/ui@2.1.9)(terser@5.44.0) vue-tsc: specifier: ^3.1.3 version: 3.1.3(typescript@5.9.3) @@ -342,6 +348,12 @@ packages: resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} deprecated: 'Merged into tsx: https://tsx.is' + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -354,6 +366,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -366,6 +384,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -378,6 +402,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -390,6 +420,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -402,6 +438,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -414,6 +456,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -426,6 +474,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -438,6 +492,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -450,6 +510,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -462,6 +528,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -474,6 +546,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -486,6 +564,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -498,6 +582,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -510,6 +600,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -522,6 +618,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -534,6 +636,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -552,6 +660,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -570,6 +684,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -588,6 +708,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -600,6 +726,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -612,6 +744,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -624,6 +762,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2082,6 +2226,40 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/ui@2.1.9': + resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} + peerDependencies: + vitest: 2.1.9 + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -2294,6 +2472,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-kit@2.1.3: resolution: {integrity: sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==} engines: {node: '>=20.19.0'} @@ -2435,6 +2617,10 @@ packages: caniuse-lite@1.0.30001753: resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2442,6 +2628,10 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + 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'} @@ -2704,6 +2894,10 @@ packages: supports-color: optional: true + 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==} @@ -2971,6 +3165,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -3174,6 +3373,10 @@ packages: resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -3215,6 +3418,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -3804,6 +4010,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4225,6 +4434,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -4788,6 +5001,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -4846,6 +5062,9 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -5003,6 +5222,12 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -5010,6 +5235,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + 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.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5241,6 +5478,11 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -5299,6 +5541,37 @@ packages: vite: ^6.0.0 || ^7.0.0 vue: ^3.5.0 + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@7.1.12: resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5339,6 +5612,31 @@ packages: yaml: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + 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.1.9 + '@vitest/ui': 2.1.9 + 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 + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -5419,6 +5717,11 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} 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'} @@ -5766,102 +6069,153 @@ snapshots: '@esbuild-kit/core-utils': 3.3.2 get-tsconfig: 4.13.0 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/android-arm64@0.18.20': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm@0.18.20': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-x64@0.18.20': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/darwin-arm64@0.18.20': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-x64@0.18.20': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.18.20': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-x64@0.18.20': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/linux-arm64@0.18.20': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm@0.18.20': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-ia32@0.18.20': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-loong64@0.18.20': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-mips64el@0.18.20': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-ppc64@0.18.20': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-riscv64@0.18.20': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-s390x@0.18.20': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-x64@0.18.20': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true @@ -5871,6 +6225,9 @@ snapshots: '@esbuild/netbsd-x64@0.18.20': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true @@ -5880,6 +6237,9 @@ snapshots: '@esbuild/openbsd-x64@0.18.20': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true @@ -5889,24 +6249,36 @@ snapshots: '@esbuild/sunos-x64@0.18.20': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/win32-arm64@0.18.20': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-ia32@0.18.20': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-x64@0.18.20': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true @@ -7528,6 +7900,57 @@ snapshots: vite: 7.1.12(@types/node@24.10.0)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.10.0)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@24.10.0)(terser@5.44.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/ui@2.1.9(vitest@2.1.9)': + dependencies: + '@vitest/utils': 2.1.9 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 1.1.2 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@24.10.0)(@vitest/ui@2.1.9)(terser@5.44.0) + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -7822,6 +8245,8 @@ snapshots: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + ast-kit@2.1.3: dependencies: '@babel/parser': 7.28.5 @@ -7973,6 +8398,14 @@ snapshots: caniuse-lite@1.0.30001753: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -7980,6 +8413,8 @@ snapshots: change-case@5.4.4: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -8225,6 +8660,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-equal@1.0.1: {} deep-is@0.1.4: {} @@ -8384,6 +8821,32 @@ snapshots: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -8679,6 +9142,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.2.2: {} + exsolve@1.0.7: {} externality@1.0.2: @@ -8719,6 +9184,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9337,6 +9804,8 @@ snapshots: lodash@4.17.21: {} + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -9983,6 +10452,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + perfect-debounce@1.0.0: {} perfect-debounce@2.0.0: {} @@ -10487,6 +10958,8 @@ snapshots: shell-quote@1.8.3: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-git@3.30.0: @@ -10535,6 +11008,8 @@ snapshots: stable-hash-x@0.2.0: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} statuses@1.5.0: {} @@ -10734,6 +11209,10 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyexec@1.0.1: {} tinyglobby@0.2.15: @@ -10741,6 +11220,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -10970,6 +11455,24 @@ snapshots: dependencies: vite: 7.1.12(@types/node@24.10.0)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node@2.1.9(@types/node@24.10.0)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@24.10.0)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.2.4(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -11035,6 +11538,16 @@ snapshots: vite: 7.1.12(@types/node@24.10.0)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) + vite@5.4.21(@types/node@24.10.0)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.50.0 + optionalDependencies: + '@types/node': 24.10.0 + fsevents: 2.3.3 + terser: 5.44.0 + vite@7.1.12(@types/node@24.10.0)(jiti@1.21.7)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -11067,6 +11580,42 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitest@2.1.9(@types/node@24.10.0)(@vitest/ui@2.1.9)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.10.0)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@24.10.0)(terser@5.44.0) + vite-node: 2.1.9(@types/node@24.10.0)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.0 + '@vitest/ui': 2.1.9(vitest@2.1.9) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vscode-uri@3.1.0: {} vue-bundle-renderer@2.2.0: @@ -11143,6 +11692,11 @@ snapshots: dependencies: isexe: 3.1.1 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/src/server/utils/wgHelper.test.ts b/src/server/utils/wgHelper.test.ts new file mode 100644 index 00000000..6390cd11 --- /dev/null +++ b/src/server/utils/wgHelper.test.ts @@ -0,0 +1,384 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { ClientType } from '#db/repositories/client/types'; +import type { InterfaceType } from '#db/repositories/interface/types'; +import type { UserConfigType } from '#db/repositories/userConfig/types'; +import type { HooksType } from '#db/repositories/hooks/types'; + +// Mock exec function +const mockExec = vi.fn(); + +// Mock iptablesTemplate function +const mockIptablesTemplate = vi.fn((template: string) => template); + +// Make WG_ENV and auto-imports available globally for test environment +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).WG_ENV = { + EXPERIMENTAL_AWG: false, + OVERRIDE_AUTO_AWG: undefined, + PORT: '51821', +}; + +// Make exec and iptablesTemplate available globally (Nuxt auto-imports simulation) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).exec = mockExec; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).iptablesTemplate = mockIptablesTemplate; + +// Mock the module resolution for exec to prevent import errors +vi.mock('~/server/utils/cmd', () => ({ + exec: mockExec, +})); + +vi.mock('~/server/utils/template', () => ({ + iptablesTemplate: mockIptablesTemplate, +})); + +// Import wgHelper after setting up mocks +const { wg } = await import('./wgHelper'); + +describe('wgHelper', () => { + const mockClient: Omit = { + id: 1, + userId: 1, + interfaceId: 'wg0', + name: 'Test Client', + enabled: true, + ipv4Address: '10.8.0.2', + ipv6Address: 'fd42:42:42::2', + publicKey: 'clientPublicKey123', + preSharedKey: 'preSharedKey123', + privateKey: 'privateKey123', + serverAllowedIps: ['192.168.1.0/24'], + serverEndpoint: '192.168.1.100:51820', + allowedIps: ['0.0.0.0/0', '::/0'], + persistentKeepalive: 25, + mtu: 1420, + dns: ['1.1.1.1', '8.8.8.8'], + expiresAt: null, + preUp: '', + postUp: '', + preDown: '', + postDown: '', + }; + + const mockInterface: InterfaceType = { + name: 'wg0', + device: 'wg0', + enabled: true, + ipv4Cidr: '10.8.0.0/24', + ipv6Cidr: 'fd42:42:42::/64', + port: 51820, + mtu: 1420, + publicKey: 'serverPublicKey123', + privateKey: 'serverPrivateKey123', + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + + const mockHooks: HooksType = { + id: 'wg0', + preUp: 'echo "PreUp"', + postUp: 'iptables -A FORWARD -i wg0 -j ACCEPT', + preDown: 'echo "PreDown"', + postDown: 'iptables -D FORWARD -i wg0 -j ACCEPT', + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + + const mockUserConfig: UserConfigType = { + id: 'wg0', + host: 'vpn.example.com', + port: 51820, + defaultDns: ['1.1.1.1', '1.0.0.1'], + defaultAllowedIps: ['0.0.0.0/0', '::/0'], + defaultMtu: 1420, + defaultPersistentKeepalive: 25, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + + describe('generateServerPeer', () => { + it('should generate server peer configuration with IPv6 enabled', () => { + const result = wg.generateServerPeer(mockClient, { enableIpv6: true }); + + expect(result).toContain('# Client: Test Client (1)'); + expect(result).toContain('[Peer]'); + expect(result).toContain('PublicKey = clientPublicKey123'); + expect(result).toContain('PresharedKey = preSharedKey123'); + expect(result).toContain( + 'AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128, 192.168.1.0/24' + ); + expect(result).toContain('Endpoint = 192.168.1.100:51820'); + }); + + it('should generate server peer configuration with IPv6 disabled', () => { + const result = wg.generateServerPeer(mockClient, { enableIpv6: false }); + + expect(result).toContain('# Client: Test Client (1)'); + expect(result).toContain('[Peer]'); + expect(result).toContain('PublicKey = clientPublicKey123'); + expect(result).toContain('PresharedKey = preSharedKey123'); + expect(result).toContain('AllowedIPs = 10.8.0.2/32, 192.168.1.0/24'); + expect(result).not.toContain('fd42:42:42::2/128'); + }); + + it('should handle client without server endpoint', () => { + const clientWithoutEndpoint = { ...mockClient, serverEndpoint: null }; + const result = wg.generateServerPeer(clientWithoutEndpoint); + + expect(result).not.toContain('Endpoint ='); + }); + + it('should handle client without serverAllowedIps', () => { + const clientWithoutAllowedIps = { ...mockClient, serverAllowedIps: [] }; + const result = wg.generateServerPeer(clientWithoutAllowedIps); + + expect(result).toContain('AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128'); + }); + }); + + describe('generateServerInterface', () => { + it('should generate server interface configuration', () => { + const result = wg.generateServerInterface(mockInterface, mockHooks, { + enableIpv6: true, + }); + + expect(result).toContain('# Note: Do not edit this file directly.'); + expect(result).toContain('[Interface]'); + expect(result).toContain('PrivateKey = serverPrivateKey123'); + expect(result).toContain('Address = 10.8.0.1/24, fd42:42:42::1/64'); + expect(result).toContain('ListenPort = 51820'); + expect(result).toContain('MTU = 1420'); + }); + + it('should generate server interface configuration without IPv6', () => { + const result = wg.generateServerInterface(mockInterface, mockHooks, { + enableIpv6: false, + }); + + expect(result).toContain('Address = 10.8.0.1/24'); + expect(result).not.toContain('fd42:42:42::1/64'); + }); + }); + + describe('generateClientConfig', () => { + it('should generate client configuration with IPv6 enabled', () => { + const fullClient: ClientType = { + ...mockClient, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const result = wg.generateClientConfig( + mockInterface, + mockUserConfig, + fullClient, + { enableIpv6: true } + ); + + expect(result).toContain('[Interface]'); + expect(result).toContain('PrivateKey = privateKey123'); + expect(result).toContain('Address = 10.8.0.2/32, fd42:42:42::2/128'); + expect(result).toContain('MTU = 1420'); + expect(result).toContain('DNS = 1.1.1.1, 8.8.8.8'); + expect(result).toContain('[Peer]'); + expect(result).toContain('PublicKey = serverPublicKey123'); + expect(result).toContain('PresharedKey = preSharedKey123'); + expect(result).toContain('AllowedIPs = 0.0.0.0/0, ::/0'); + expect(result).toContain('PersistentKeepalive = 25'); + expect(result).toContain('Endpoint = vpn.example.com:51820'); + }); + + it('should generate client configuration with IPv6 disabled', () => { + const fullClient: ClientType = { + ...mockClient, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const result = wg.generateClientConfig( + mockInterface, + mockUserConfig, + fullClient, + { enableIpv6: false } + ); + + expect(result).toContain('Address = 10.8.0.2/32'); + expect(result).not.toContain('fd42:42:42::2/128'); + }); + + it('should use default DNS when client DNS is not set', () => { + const clientWithoutDns: ClientType = { + ...mockClient, + dns: null, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const result = wg.generateClientConfig( + mockInterface, + mockUserConfig, + clientWithoutDns + ); + + expect(result).toContain('DNS = 1.1.1.1, 1.0.0.1'); + }); + + it('should not include DNS line when DNS is empty', () => { + const clientWithoutDns: ClientType = { + ...mockClient, + dns: null, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const configWithoutDns = { ...mockUserConfig, defaultDns: [] }; + const result = wg.generateClientConfig( + mockInterface, + configWithoutDns, + clientWithoutDns + ); + + expect(result).not.toContain('DNS ='); + }); + + it('should include client hooks when present', () => { + const clientWithHooks: ClientType = { + ...mockClient, + preUp: 'echo "Client PreUp"', + postUp: 'echo "Client PostUp"', + preDown: 'echo "Client PreDown"', + postDown: 'echo "Client PostDown"', + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const result = wg.generateClientConfig( + mockInterface, + mockUserConfig, + clientWithHooks + ); + + expect(result).toContain('PreUp = echo "Client PreUp"'); + expect(result).toContain('PostUp = echo "Client PostUp"'); + expect(result).toContain('PreDown = echo "Client PreDown"'); + expect(result).toContain('PostDown = echo "Client PostDown"'); + }); + + it('should use default allowedIps when client allowedIps is not set', () => { + const clientWithoutAllowedIps: ClientType = { + ...mockClient, + allowedIps: null, + createdAt: '2023-01-01T00:00:00.000Z', + updatedAt: '2023-01-01T00:00:00.000Z', + }; + const result = wg.generateClientConfig( + mockInterface, + mockUserConfig, + clientWithoutAllowedIps + ); + + expect(result).toContain('AllowedIPs = 0.0.0.0/0, ::/0'); + }); + }); + + describe('dump', () => { + it('should parse wireguard dump output correctly', async () => { + const mockDumpOutput = `private-key public-key listen-port fwmark +publicKey1 psk1 192.168.1.100:12345 10.8.0.2/32 1699000000 1000000 2000000 25 +publicKey2 (none) (none) 10.8.0.3/32 0 500000 1500000 0`; + + mockExec.mockResolvedValue(mockDumpOutput); + + const result = await wg.dump('wg0'); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + publicKey: 'publicKey1', + preSharedKey: 'psk1', + endpoint: '192.168.1.100:12345', + allowedIps: '10.8.0.2/32', + latestHandshakeAt: new Date(1699000000000), + transferRx: 1000000, + transferTx: 2000000, + persistentKeepalive: '25', + }); + expect(result[1]).toEqual({ + publicKey: 'publicKey2', + preSharedKey: '(none)', + endpoint: null, + allowedIps: '10.8.0.3/32', + latestHandshakeAt: null, + transferRx: 500000, + transferTx: 1500000, + persistentKeepalive: '0', + }); + }); + }); + + describe('command execution functions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should execute generatePrivateKey command', async () => { + mockExec.mockResolvedValue('generatedPrivateKey'); + + const result = await wg.generatePrivateKey(); + + expect(result).toBe('generatedPrivateKey'); + expect(mockExec).toHaveBeenCalledWith('wg genkey'); + }); + + it('should execute getPublicKey command', async () => { + mockExec.mockResolvedValue('generatedPublicKey'); + + const result = await wg.getPublicKey('privateKey123'); + + expect(result).toBe('generatedPublicKey'); + expect(mockExec).toHaveBeenCalledWith('echo privateKey123 | wg pubkey', { + log: 'echo ***hidden*** | wg pubkey', + }); + }); + + it('should execute generatePreSharedKey command', async () => { + mockExec.mockResolvedValue('generatedPSK'); + + const result = await wg.generatePreSharedKey(); + + expect(result).toBe('generatedPSK'); + expect(mockExec).toHaveBeenCalledWith('wg genpsk'); + }); + + it('should execute up command', async () => { + mockExec.mockResolvedValue(''); + + await wg.up('wg0'); + + expect(mockExec).toHaveBeenCalledWith('wg-quick up wg0'); + }); + + it('should execute down command', async () => { + mockExec.mockResolvedValue(''); + + await wg.down('wg0'); + + expect(mockExec).toHaveBeenCalledWith('wg-quick down wg0'); + }); + + it('should execute restart command', async () => { + mockExec.mockResolvedValue(''); + + await wg.restart('wg0'); + + expect(mockExec).toHaveBeenCalledWith( + 'wg-quick down wg0; wg-quick up wg0' + ); + }); + + it('should execute sync command', async () => { + mockExec.mockResolvedValue(''); + + await wg.sync('wg0'); + + expect(mockExec).toHaveBeenCalledWith( + 'wg syncconf wg0 <(wg-quick strip wg0)' + ); + }); + }); +}); diff --git a/src/vitest.config.ts b/src/vitest.config.ts new file mode 100644 index 00000000..fbf1ffce --- /dev/null +++ b/src/vitest.config.ts @@ -0,0 +1,22 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + pool: 'threads', + poolOptions: { + threads: { + singleThread: true, + }, + }, + }, + resolve: { + alias: { + '#db': resolve(__dirname, './server/database'), + '~': resolve(__dirname, './'), + '@': resolve(__dirname, './'), + }, + }, +});