mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Merge pull request #92 from ArnasDon/test/foundational-vitest-suite
test: foundational Vitest suite for security + critical pure logic
This commit is contained in:
commit
e80189ec97
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -14,16 +14,21 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Lint, typecheck, build
|
name: Lint, typecheck, test, build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Dummy env vars so `next build` doesn't fail when it reads the
|
# Dummy env vars so `next build` doesn't fail when it reads the
|
||||||
# public Supabase config at build time. These never leave CI and
|
# public Supabase config at build time. These never leave CI and
|
||||||
# never hit a real service — they just satisfy the `!` non-null
|
# never hit a real service — they just satisfy the `!` non-null
|
||||||
# assertions in the client factories.
|
# assertions in the client factories. ENCRYPTION_KEY and
|
||||||
|
# META_APP_SECRET are read at module-load by lib/whatsapp/*; the
|
||||||
|
# test suite asserts behaviour around these values, so any non-
|
||||||
|
# empty placeholder works as long as it stays consistent with
|
||||||
|
# vitest.config.ts.
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_SUPABASE_URL: https://ci.example.supabase.co
|
NEXT_PUBLIC_SUPABASE_URL: https://ci.example.supabase.co
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-dummy-anon-key
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-dummy-anon-key
|
||||||
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
|
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
META_APP_SECRET: 'ci-dummy-meta-secret'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
@ -41,5 +46,8 @@ jobs:
|
|||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|||||||
813
package-lock.json
generated
813
package-lock.json
generated
@ -37,7 +37,8 @@
|
|||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^6"
|
"typescript": "^6",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
@ -1996,6 +1997,299 @@
|
|||||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.130.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
|
||||||
|
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "1.10.0",
|
||||||
|
"@emnapi/runtime": "1.10.0",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@ -2021,6 +2315,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@supabase/auth-js": {
|
"node_modules/@supabase/auth-js": {
|
||||||
"version": "2.105.4",
|
"version": "2.105.4",
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.4.tgz",
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.4.tgz",
|
||||||
@ -2560,6 +2861,24 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@ -3187,6 +3506,119 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.1.6",
|
||||||
|
"@vitest/utils": "4.1.6",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.1.6",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.1.6",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.6",
|
||||||
|
"@vitest/utils": "4.1.6",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.6",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@ -3491,6 +3923,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types": {
|
"node_modules/ast-types": {
|
||||||
"version": "0.16.1",
|
"version": "0.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz",
|
||||||
@ -3756,6 +4198,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@ -4532,6 +4984,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
@ -5028,6 +5487,16 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
@ -5094,6 +5563,16 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
@ -5412,6 +5891,21 @@
|
|||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -7627,6 +8121,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@ -7881,6 +8386,13 @@
|
|||||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -7919,9 +8431,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -8394,6 +8906,40 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rolldown": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oxc-project/types": "=0.130.0",
|
||||||
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rolldown": "bin/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rolldown/binding-android-arm64": "1.0.1",
|
||||||
|
"@rolldown/binding-darwin-arm64": "1.0.1",
|
||||||
|
"@rolldown/binding-darwin-x64": "1.0.1",
|
||||||
|
"@rolldown/binding-freebsd-x64": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-arm64-musl": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-x64-gnu": "1.0.1",
|
||||||
|
"@rolldown/binding-linux-x64-musl": "1.0.1",
|
||||||
|
"@rolldown/binding-openharmony-arm64": "1.0.1",
|
||||||
|
"@rolldown/binding-wasm32-wasi": "1.0.1",
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
|
||||||
|
"@rolldown/binding-win32-x64-msvc": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/router": {
|
"node_modules/router": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
@ -8878,6 +9424,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
@ -8931,6 +9484,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@ -8940,6 +9500,13 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stdin-discarder": {
|
"node_modules/stdin-discarder": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||||
@ -9272,6 +9839,23 @@
|
|||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
@ -9320,6 +9904,16 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tldts": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.28",
|
"version": "7.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
|
||||||
@ -9766,6 +10360,200 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "8.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||||
|
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"postcss": "^8.5.14",
|
||||||
|
"rolldown": "1.0.1",
|
||||||
|
"tinyglobby": "^0.2.16"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"@vitejs/devtools": "^0.1.18",
|
||||||
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitejs/devtools": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
||||||
|
"integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.1.6",
|
||||||
|
"@vitest/mocker": "4.1.6",
|
||||||
|
"@vitest/pretty-format": "4.1.6",
|
||||||
|
"@vitest/runner": "4.1.6",
|
||||||
|
"@vitest/snapshot": "4.1.6",
|
||||||
|
"@vitest/spy": "4.1.6",
|
||||||
|
"@vitest/utils": "4.1.6",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"expect-type": "^1.3.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.1.0",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.1.6",
|
||||||
|
"@vitest/browser-preview": "4.1.6",
|
||||||
|
"@vitest/browser-webdriverio": "4.1.6",
|
||||||
|
"@vitest/coverage-istanbul": "4.1.6",
|
||||||
|
"@vitest/coverage-v8": "4.1.6",
|
||||||
|
"@vitest/ui": "4.1.6",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest/node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
@ -9879,6 +10667,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@ -34,7 +34,9 @@
|
|||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check .",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
@ -65,6 +67,7 @@
|
|||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^6"
|
"typescript": "^6",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
236
src/lib/automations/validate.test.ts
Normal file
236
src/lib/automations/validate.test.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
validateStepsForActivation,
|
||||||
|
validateTriggerForActivation,
|
||||||
|
} from "./validate";
|
||||||
|
|
||||||
|
describe("validateStepsForActivation", () => {
|
||||||
|
it("rejects empty or missing step lists", () => {
|
||||||
|
expect(validateStepsForActivation([])).toEqual([
|
||||||
|
{ path: "steps", message: "active automations need at least one step" },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
validateStepsForActivation(undefined as unknown as never[]),
|
||||||
|
).toEqual([
|
||||||
|
{ path: "steps", message: "active automations need at least one step" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes a fully-populated step set", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "send_message", step_config: { text: "hi" } },
|
||||||
|
{
|
||||||
|
step_type: "wait",
|
||||||
|
step_config: { amount: 5, unit: "minutes" },
|
||||||
|
},
|
||||||
|
{ step_type: "add_tag", step_config: { tag_id: "tag-uuid" } },
|
||||||
|
{ step_type: "close_conversation", step_config: {} },
|
||||||
|
]);
|
||||||
|
expect(issues).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags every required field that is missing", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "send_message", step_config: { text: " " } },
|
||||||
|
{ step_type: "send_template", step_config: {} },
|
||||||
|
{ step_type: "add_tag", step_config: { tag_id: "" } },
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path)).toEqual([
|
||||||
|
"steps[0].text",
|
||||||
|
"steps[1].template_name",
|
||||||
|
"steps[2].tag_id",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks wait amount and unit boundaries", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "wait", step_config: { amount: 0, unit: "minutes" } },
|
||||||
|
{ step_type: "wait", step_config: { amount: 5, unit: "seconds" } },
|
||||||
|
{ step_type: "wait", step_config: { amount: -1, unit: "hours" } },
|
||||||
|
{
|
||||||
|
step_type: "wait",
|
||||||
|
step_config: { amount: Number.POSITIVE_INFINITY, unit: "days" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path)).toEqual([
|
||||||
|
"steps[0].amount",
|
||||||
|
"steps[1].unit",
|
||||||
|
"steps[2].amount",
|
||||||
|
"steps[3].amount",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates webhook URLs", () => {
|
||||||
|
const good = validateStepsForActivation([
|
||||||
|
{
|
||||||
|
step_type: "send_webhook",
|
||||||
|
step_config: { url: "https://hooks.example.com/in" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(good).toEqual([]);
|
||||||
|
|
||||||
|
const noUrl = validateStepsForActivation([
|
||||||
|
{ step_type: "send_webhook", step_config: {} },
|
||||||
|
]);
|
||||||
|
expect(noUrl.map((i) => i.message)).toContain("webhook URL is required");
|
||||||
|
|
||||||
|
const wrongProtocol = validateStepsForActivation([
|
||||||
|
{
|
||||||
|
step_type: "send_webhook",
|
||||||
|
step_config: { url: "ftp://files.example.com" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(wrongProtocol.map((i) => i.message)).toContain(
|
||||||
|
"webhook URL must use http or https",
|
||||||
|
);
|
||||||
|
|
||||||
|
const garbage = validateStepsForActivation([
|
||||||
|
{ step_type: "send_webhook", step_config: { url: "not a url" } },
|
||||||
|
]);
|
||||||
|
expect(garbage.map((i) => i.message)).toContain(
|
||||||
|
"webhook URL is not a valid URL",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates assign_conversation only when mode is 'specific'", () => {
|
||||||
|
const roundRobinNoAgent = validateStepsForActivation([
|
||||||
|
{
|
||||||
|
step_type: "assign_conversation",
|
||||||
|
step_config: { mode: "round_robin" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(roundRobinNoAgent).toEqual([]);
|
||||||
|
|
||||||
|
const specificMissingAgent = validateStepsForActivation([
|
||||||
|
{ step_type: "assign_conversation", step_config: { mode: "specific" } },
|
||||||
|
]);
|
||||||
|
expect(specificMissingAgent.map((i) => i.path)).toEqual([
|
||||||
|
"steps[0].agent_id",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags create_deal when required fields are missing", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "create_deal", step_config: {} },
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path).sort()).toEqual([
|
||||||
|
"steps[0].pipeline_id",
|
||||||
|
"steps[0].stage_id",
|
||||||
|
"steps[0].title",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags update_contact_field when field or value is missing", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "update_contact_field", step_config: { field: "name" } },
|
||||||
|
{
|
||||||
|
step_type: "update_contact_field",
|
||||||
|
step_config: { field: "", value: "x" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path)).toEqual([
|
||||||
|
"steps[0].value",
|
||||||
|
"steps[1].field",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recursively walks condition branches with stable dot-paths", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{
|
||||||
|
step_type: "condition",
|
||||||
|
step_config: { subject: "tag", operand: "vip" },
|
||||||
|
branches: {
|
||||||
|
yes: [{ step_type: "add_tag", step_config: { tag_id: "" } }],
|
||||||
|
no: [
|
||||||
|
{
|
||||||
|
step_type: "send_message",
|
||||||
|
step_config: { text: "" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path)).toEqual([
|
||||||
|
"steps[0].yes.steps[0].tag_id",
|
||||||
|
"steps[0].no.steps[0].text",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an issue for unknown step types", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "do_a_barrel_roll", step_config: {} },
|
||||||
|
]);
|
||||||
|
expect(issues).toEqual([
|
||||||
|
{ path: "steps[0]", message: "unknown step type: do_a_barrel_roll" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags condition subject/operand independently", () => {
|
||||||
|
const issues = validateStepsForActivation([
|
||||||
|
{ step_type: "condition", step_config: {} },
|
||||||
|
]);
|
||||||
|
expect(issues.map((i) => i.path).sort()).toEqual([
|
||||||
|
"steps[0].operand",
|
||||||
|
"steps[0].subject",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateTriggerForActivation", () => {
|
||||||
|
it("accepts a valid keyword_match config", () => {
|
||||||
|
expect(
|
||||||
|
validateTriggerForActivation("keyword_match", {
|
||||||
|
keywords: ["hello", "hi"],
|
||||||
|
match_type: "exact",
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects keyword_match with empty keyword array", () => {
|
||||||
|
const issues = validateTriggerForActivation("keyword_match", {
|
||||||
|
keywords: [],
|
||||||
|
match_type: "exact",
|
||||||
|
});
|
||||||
|
expect(issues.map((i) => i.path)).toContain("trigger.keywords");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects keyword_match with whitespace-only entries", () => {
|
||||||
|
const issues = validateTriggerForActivation("keyword_match", {
|
||||||
|
keywords: ["hi", " "],
|
||||||
|
match_type: "contains",
|
||||||
|
});
|
||||||
|
expect(issues.map((i) => i.message)).toContain(
|
||||||
|
"keywords cannot be empty strings",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects keyword_match with an unknown match_type", () => {
|
||||||
|
const issues = validateTriggerForActivation("keyword_match", {
|
||||||
|
keywords: ["hi"],
|
||||||
|
match_type: "fuzzy",
|
||||||
|
});
|
||||||
|
expect(issues.map((i) => i.path)).toContain("trigger.match_type");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires schedule on time_based triggers", () => {
|
||||||
|
expect(validateTriggerForActivation("time_based", {})).toEqual([
|
||||||
|
{ path: "trigger.schedule", message: "schedule is required" },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
validateTriggerForActivation("time_based", { schedule: "0 9 * * *" }),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires tag_id on tag_added triggers", () => {
|
||||||
|
expect(validateTriggerForActivation("tag_added", {})).toEqual([
|
||||||
|
{ path: "trigger.tag_id", message: "tag is required" },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
validateTriggerForActivation("tag_added", { tag_id: "tag-uuid" }),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not flag unknown trigger types (handled elsewhere)", () => {
|
||||||
|
expect(validateTriggerForActivation("some_future_trigger", {})).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/lib/broadcast-status.test.ts
Normal file
48
src/lib/broadcast-status.test.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
broadcastStatusConfig,
|
||||||
|
getBroadcastStatus,
|
||||||
|
getRecipientStatus,
|
||||||
|
recipientStatusConfig,
|
||||||
|
} from "./broadcast-status";
|
||||||
|
|
||||||
|
describe("getBroadcastStatus", () => {
|
||||||
|
it("returns the matching config for known statuses", () => {
|
||||||
|
expect(getBroadcastStatus("sending")).toBe(broadcastStatusConfig.sending);
|
||||||
|
expect(getBroadcastStatus("sent")).toBe(broadcastStatusConfig.sent);
|
||||||
|
expect(getBroadcastStatus("failed")).toBe(broadcastStatusConfig.failed);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags `sending` as a live/pulsing state", () => {
|
||||||
|
expect(getBroadcastStatus("sending").pulse).toBe(true);
|
||||||
|
expect(getBroadcastStatus("sent").pulse).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to draft on an unknown status string", () => {
|
||||||
|
expect(getBroadcastStatus("not-a-real-status")).toBe(
|
||||||
|
broadcastStatusConfig.draft,
|
||||||
|
);
|
||||||
|
expect(getBroadcastStatus("")).toBe(broadcastStatusConfig.draft);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("each variant has the dark-theme class triple", () => {
|
||||||
|
for (const v of Object.values(broadcastStatusConfig)) {
|
||||||
|
expect(v.classes).toMatch(/bg-[a-z]+-\d+\/10/);
|
||||||
|
expect(v.classes).toMatch(/text-[a-z]+-\d+/);
|
||||||
|
expect(v.classes).toMatch(/border-[a-z]+-\d+\/20/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRecipientStatus", () => {
|
||||||
|
it("returns the matching config for known statuses", () => {
|
||||||
|
expect(getRecipientStatus("delivered")).toBe(
|
||||||
|
recipientStatusConfig.delivered,
|
||||||
|
);
|
||||||
|
expect(getRecipientStatus("read")).toBe(recipientStatusConfig.read);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to pending on an unknown status string", () => {
|
||||||
|
expect(getRecipientStatus("???")).toBe(recipientStatusConfig.pending);
|
||||||
|
});
|
||||||
|
});
|
||||||
123
src/lib/dashboard/date-utils.test.ts
Normal file
123
src/lib/dashboard/date-utils.test.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
DOW_SHORT_MON_FIRST,
|
||||||
|
daysAgoStart,
|
||||||
|
lastNDayKeys,
|
||||||
|
localDayKey,
|
||||||
|
mondayIndex,
|
||||||
|
startOfLocalDay,
|
||||||
|
} from "./date-utils";
|
||||||
|
|
||||||
|
describe("startOfLocalDay", () => {
|
||||||
|
it("zeroes out the time of a given date", () => {
|
||||||
|
const d = new Date("2026-05-18T13:45:22.500");
|
||||||
|
const out = startOfLocalDay(d);
|
||||||
|
expect(out.getHours()).toBe(0);
|
||||||
|
expect(out.getMinutes()).toBe(0);
|
||||||
|
expect(out.getSeconds()).toBe(0);
|
||||||
|
expect(out.getMilliseconds()).toBe(0);
|
||||||
|
expect(out.getFullYear()).toBe(d.getFullYear());
|
||||||
|
expect(out.getMonth()).toBe(d.getMonth());
|
||||||
|
expect(out.getDate()).toBe(d.getDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate the input", () => {
|
||||||
|
const d = new Date("2026-05-18T13:45:22.500");
|
||||||
|
const before = d.getTime();
|
||||||
|
startOfLocalDay(d);
|
||||||
|
expect(d.getTime()).toBe(before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("daysAgoStart", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-05-18T13:45:22"));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns midnight N days before today", () => {
|
||||||
|
const out = daysAgoStart(3);
|
||||||
|
expect(out.getHours()).toBe(0);
|
||||||
|
expect(out.getDate()).toBe(15);
|
||||||
|
expect(out.getMonth()).toBe(4); // May
|
||||||
|
expect(out.getFullYear()).toBe(2026);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("daysAgoStart(0) is today at midnight", () => {
|
||||||
|
const out = daysAgoStart(0);
|
||||||
|
expect(out.getDate()).toBe(18);
|
||||||
|
expect(out.getHours()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("crosses month boundaries cleanly", () => {
|
||||||
|
vi.setSystemTime(new Date("2026-05-02T08:00:00"));
|
||||||
|
const out = daysAgoStart(5);
|
||||||
|
expect(out.getMonth()).toBe(3); // April (0-indexed)
|
||||||
|
expect(out.getDate()).toBe(27);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("localDayKey", () => {
|
||||||
|
it("emits YYYY-MM-DD in local components", () => {
|
||||||
|
const d = new Date(2026, 0, 9, 23, 59); // Jan 9, locally
|
||||||
|
expect(localDayKey(d)).toBe("2026-01-09");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zero-pads month and day", () => {
|
||||||
|
const d = new Date(2026, 8, 5); // Sep 5
|
||||||
|
expect(localDayKey(d)).toBe("2026-09-05");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts ISO strings as input", () => {
|
||||||
|
expect(localDayKey("2026-12-31T23:00:00")).toBe("2026-12-31");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("lastNDayKeys", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-05-18T08:30:00"));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns n consecutive chronological keys ending today", () => {
|
||||||
|
expect(lastNDayKeys(3)).toEqual(["2026-05-16", "2026-05-17", "2026-05-18"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns just today for n=1", () => {
|
||||||
|
expect(lastNDayKeys(1)).toEqual(["2026-05-18"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls back across a month boundary", () => {
|
||||||
|
vi.setSystemTime(new Date("2026-05-02T08:00:00"));
|
||||||
|
expect(lastNDayKeys(4)).toEqual([
|
||||||
|
"2026-04-29",
|
||||||
|
"2026-04-30",
|
||||||
|
"2026-05-01",
|
||||||
|
"2026-05-02",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mondayIndex", () => {
|
||||||
|
it("maps Monday → 0 and Sunday → 6", () => {
|
||||||
|
expect(mondayIndex(new Date("2026-05-18"))).toBe(0); // Mon
|
||||||
|
expect(mondayIndex(new Date("2026-05-19"))).toBe(1); // Tue
|
||||||
|
expect(mondayIndex(new Date("2026-05-23"))).toBe(5); // Sat
|
||||||
|
expect(mondayIndex(new Date("2026-05-24"))).toBe(6); // Sun
|
||||||
|
});
|
||||||
|
|
||||||
|
it("aligns with DOW_SHORT_MON_FIRST labels", () => {
|
||||||
|
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date("2026-05-18"))]).toBe(
|
||||||
|
"Mon",
|
||||||
|
);
|
||||||
|
expect(DOW_SHORT_MON_FIRST[mondayIndex(new Date("2026-05-24"))]).toBe(
|
||||||
|
"Sun",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/lib/rate-limit.test.ts
Normal file
109
src/lib/rate-limit.test.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
__resetRateLimitForTests,
|
||||||
|
checkRateLimit,
|
||||||
|
rateLimitResponse,
|
||||||
|
} from "./rate-limit";
|
||||||
|
|
||||||
|
const OPTS = { limit: 3, windowMs: 60_000 };
|
||||||
|
|
||||||
|
describe("checkRateLimit", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
__resetRateLimitForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("permits the first request and decrements remaining", () => {
|
||||||
|
const result = checkRateLimit("user:1", OPTS);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
remaining: 2,
|
||||||
|
limit: 3,
|
||||||
|
});
|
||||||
|
expect(result.reset).toBeGreaterThan(Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("permits exactly `limit` requests then rejects the next", () => {
|
||||||
|
expect(checkRateLimit("user:1", OPTS).success).toBe(true);
|
||||||
|
expect(checkRateLimit("user:1", OPTS).success).toBe(true);
|
||||||
|
expect(checkRateLimit("user:1", OPTS).success).toBe(true);
|
||||||
|
const over = checkRateLimit("user:1", OPTS);
|
||||||
|
expect(over.success).toBe(false);
|
||||||
|
expect(over.remaining).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps separate counters per key", () => {
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
// user:1 is at the cap, user:2 should still be unaffected.
|
||||||
|
const other = checkRateLimit("user:2", OPTS);
|
||||||
|
expect(other.success).toBe(true);
|
||||||
|
expect(other.remaining).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a fresh window after `windowMs` elapses", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const t0 = new Date("2026-05-01T00:00:00Z").getTime();
|
||||||
|
vi.setSystemTime(t0);
|
||||||
|
__resetRateLimitForTests();
|
||||||
|
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
checkRateLimit("user:1", OPTS);
|
||||||
|
expect(checkRateLimit("user:1", OPTS).success).toBe(false);
|
||||||
|
|
||||||
|
// Jump just past the window.
|
||||||
|
vi.setSystemTime(t0 + OPTS.windowMs + 1);
|
||||||
|
const refreshed = checkRateLimit("user:1", OPTS);
|
||||||
|
expect(refreshed.success).toBe(true);
|
||||||
|
expect(refreshed.remaining).toBe(2);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rateLimitResponse", () => {
|
||||||
|
it("returns a 429 with retry / X-RateLimit headers", async () => {
|
||||||
|
const reset = Date.now() + 30_000;
|
||||||
|
const res = rateLimitResponse({
|
||||||
|
success: false,
|
||||||
|
remaining: 0,
|
||||||
|
reset,
|
||||||
|
limit: 60,
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(429);
|
||||||
|
expect(res.headers.get("X-RateLimit-Limit")).toBe("60");
|
||||||
|
expect(res.headers.get("X-RateLimit-Remaining")).toBe("0");
|
||||||
|
expect(Number(res.headers.get("Retry-After"))).toBeGreaterThan(0);
|
||||||
|
const body = (await res.json()) as { error: string };
|
||||||
|
expect(body.error).toMatch(/rate limit/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps Retry-After to a minimum of 1 second", () => {
|
||||||
|
// Reset already in the past — the ceiling math would otherwise give 0.
|
||||||
|
const res = rateLimitResponse({
|
||||||
|
success: false,
|
||||||
|
remaining: 0,
|
||||||
|
reset: Date.now() - 5_000,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
expect(Number(res.headers.get("Retry-After"))).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RATE_LIMITS presets", () => {
|
||||||
|
it("send and broadcast budgets are independent", async () => {
|
||||||
|
__resetRateLimitForTests();
|
||||||
|
// Importing here so the presets stay close to their assertions.
|
||||||
|
const { RATE_LIMITS } = await import("./rate-limit");
|
||||||
|
expect(RATE_LIMITS.send.limit).toBeGreaterThan(RATE_LIMITS.broadcast.limit);
|
||||||
|
expect(RATE_LIMITS.send.windowMs).toBe(60_000);
|
||||||
|
expect(RATE_LIMITS.broadcast.windowMs).toBe(60_000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
__resetRateLimitForTests();
|
||||||
|
});
|
||||||
125
src/lib/whatsapp/encryption.test.ts
Normal file
125
src/lib/whatsapp/encryption.test.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { decrypt, encrypt, isLegacyFormat } from "./encryption";
|
||||||
|
|
||||||
|
const KEY_HEX = process.env.ENCRYPTION_KEY!;
|
||||||
|
|
||||||
|
function cbcEncryptLegacy(plaintext: string): string {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv(
|
||||||
|
"aes-256-cbc",
|
||||||
|
Buffer.from(KEY_HEX, "hex"),
|
||||||
|
iv,
|
||||||
|
);
|
||||||
|
let ct = cipher.update(plaintext, "utf8", "hex");
|
||||||
|
ct += cipher.final("hex");
|
||||||
|
return `${iv.toString("hex")}:${ct}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("encryption", () => {
|
||||||
|
describe("encrypt / decrypt round-trip", () => {
|
||||||
|
it("recovers the original plaintext", () => {
|
||||||
|
const ct = encrypt("EAAG... fake WhatsApp token");
|
||||||
|
expect(decrypt(ct)).toBe("EAAG... fake WhatsApp token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces three colon-separated parts (GCM)", () => {
|
||||||
|
const ct = encrypt("anything");
|
||||||
|
expect(ct.split(":")).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a fresh IV per encrypt so identical plaintexts produce different ciphertexts", () => {
|
||||||
|
const a = encrypt("same input");
|
||||||
|
const b = encrypt("same input");
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
expect(decrypt(a)).toBe("same input");
|
||||||
|
expect(decrypt(b)).toBe("same input");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips empty string", () => {
|
||||||
|
const ct = encrypt("");
|
||||||
|
expect(decrypt(ct)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips multibyte UTF-8", () => {
|
||||||
|
const ct = encrypt("token-✓-🔐-žąsis");
|
||||||
|
expect(decrypt(ct)).toBe("token-✓-🔐-žąsis");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GCM authentication", () => {
|
||||||
|
it("rejects ciphertext tampered after encryption", () => {
|
||||||
|
const ct = encrypt("secret");
|
||||||
|
const [ivHex, ctHex, tagHex] = ct.split(":");
|
||||||
|
// Flip a byte in the ciphertext body — auth tag will mismatch.
|
||||||
|
const tamperedCtHex =
|
||||||
|
(parseInt(ctHex.slice(0, 2), 16) ^ 0xff).toString(16).padStart(2, "0") +
|
||||||
|
ctHex.slice(2);
|
||||||
|
expect(() =>
|
||||||
|
decrypt(`${ivHex}:${tamperedCtHex}:${tagHex}`),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a swapped auth tag", () => {
|
||||||
|
const ct = encrypt("secret");
|
||||||
|
const [ivHex, ctHex] = ct.split(":");
|
||||||
|
const bogusTag = "00".repeat(16);
|
||||||
|
expect(() => decrypt(`${ivHex}:${ctHex}:${bogusTag}`)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a GCM IV of the wrong length", () => {
|
||||||
|
const ct = encrypt("secret");
|
||||||
|
const [, ctHex, tagHex] = ct.split(":");
|
||||||
|
const shortIv = "00".repeat(8); // 8 bytes ≠ 12
|
||||||
|
expect(() => decrypt(`${shortIv}:${ctHex}:${tagHex}`)).toThrow(
|
||||||
|
/GCM IV length/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a GCM auth tag of the wrong length", () => {
|
||||||
|
const ct = encrypt("secret");
|
||||||
|
const [ivHex, ctHex] = ct.split(":");
|
||||||
|
const shortTag = "00".repeat(8); // 8 bytes ≠ 16
|
||||||
|
expect(() => decrypt(`${ivHex}:${ctHex}:${shortTag}`)).toThrow(
|
||||||
|
/auth-tag length/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("legacy CBC compatibility (read-only)", () => {
|
||||||
|
it("decrypts a CBC blob produced by the previous codepath", () => {
|
||||||
|
const legacy = cbcEncryptLegacy("old-token");
|
||||||
|
expect(decrypt(legacy)).toBe("old-token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a CBC blob with the wrong IV length", () => {
|
||||||
|
// 8-byte IV (16 hex chars) instead of 16 bytes.
|
||||||
|
const bogus = "00".repeat(8) + ":" + "00".repeat(16);
|
||||||
|
expect(() => decrypt(bogus)).toThrow(/CBC IV length/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("format detection", () => {
|
||||||
|
it("isLegacyFormat returns true for two-part CBC strings", () => {
|
||||||
|
const legacy = cbcEncryptLegacy("anything");
|
||||||
|
expect(isLegacyFormat(legacy)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isLegacyFormat returns false for three-part GCM strings", () => {
|
||||||
|
const modern = encrypt("anything");
|
||||||
|
expect(isLegacyFormat(modern)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("malformed input", () => {
|
||||||
|
it("throws on a single-token blob (no colons)", () => {
|
||||||
|
expect(() => decrypt("not-encrypted-at-all")).toThrow(
|
||||||
|
/unrecognised format/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on a four-part blob", () => {
|
||||||
|
expect(() => decrypt("aa:bb:cc:dd")).toThrow(/unrecognised format/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
164
src/lib/whatsapp/phone-utils.test.ts
Normal file
164
src/lib/whatsapp/phone-utils.test.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
isRecipientNotAllowedError,
|
||||||
|
isValidE164,
|
||||||
|
normalizePhone,
|
||||||
|
phoneVariants,
|
||||||
|
phonesMatch,
|
||||||
|
sanitizePhoneForMeta,
|
||||||
|
} from "./phone-utils";
|
||||||
|
|
||||||
|
describe("sanitizePhoneForMeta", () => {
|
||||||
|
it("strips +, spaces, and dashes leaving only digits", () => {
|
||||||
|
expect(sanitizePhoneForMeta("+370 639 49836")).toBe("37063949836");
|
||||||
|
expect(sanitizePhoneForMeta("+1 (415) 555-1212")).toBe("14155551212");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string for falsy input", () => {
|
||||||
|
expect(sanitizePhoneForMeta("")).toBe("");
|
||||||
|
// Defensive: existing call sites occasionally pass through nullable
|
||||||
|
// contact phones. The function early-returns on the falsy check.
|
||||||
|
expect(sanitizePhoneForMeta(undefined as unknown as string)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is idempotent on already-sanitized input", () => {
|
||||||
|
const cleaned = "14155551212";
|
||||||
|
expect(sanitizePhoneForMeta(cleaned)).toBe(cleaned);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizePhone", () => {
|
||||||
|
it("matches sanitizePhoneForMeta byte-for-byte (shared canonical form)", () => {
|
||||||
|
const samples = ["+370 12345", "abc-555-DEF", "", "0044 7000 0000 0000"];
|
||||||
|
for (const s of samples) {
|
||||||
|
expect(normalizePhone(s)).toBe(sanitizePhoneForMeta(s));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("phonesMatch", () => {
|
||||||
|
it("returns true for exact digit matches", () => {
|
||||||
|
expect(phonesMatch("+37063949836", "37063949836")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches across trunk-prefix variants by last-8 fallback", () => {
|
||||||
|
// Lithuanian trunk-0 variant. Last 8 digits ("63949836") collide.
|
||||||
|
expect(phonesMatch("370063949836", "37063949836")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects mismatched numbers", () => {
|
||||||
|
expect(phonesMatch("+37063949836", "+37063949837")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects very short inputs that would false-positive on tail match", () => {
|
||||||
|
// Only 7 digits — the last-8 fallback is gated to len>=8 on both
|
||||||
|
// sides to avoid declaring "12345" and "67890-12345" a match.
|
||||||
|
expect(phonesMatch("1234567", "1234567")).toBe(true);
|
||||||
|
expect(phonesMatch("1234567", "9991234567")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores formatting noise on both sides", () => {
|
||||||
|
expect(phonesMatch("+370 6 394 9836", "37063949836")).toBe(true);
|
||||||
|
expect(phonesMatch("(415) 555-1212", "+1 415-555-1212")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidE164", () => {
|
||||||
|
it("accepts numbers 7–15 digits with optional + and non-zero start", () => {
|
||||||
|
expect(isValidE164("+37063949836")).toBe(true);
|
||||||
|
expect(isValidE164("37063949836")).toBe(true);
|
||||||
|
expect(isValidE164("+1234567")).toBe(true); // 7 digits — lower bound
|
||||||
|
expect(isValidE164("+123456789012345")).toBe(true); // 15 digits — upper bound
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects numbers that start with 0 in international form", () => {
|
||||||
|
expect(isValidE164("+0123456")).toBe(false);
|
||||||
|
expect(isValidE164("0044700000000")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects too-short and too-long inputs", () => {
|
||||||
|
expect(isValidE164("+123456")).toBe(false); // 6 digits
|
||||||
|
expect(isValidE164("+1234567890123456")).toBe(false); // 16 digits
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects strings with non-digit characters", () => {
|
||||||
|
expect(isValidE164("+1-415-555-1212")).toBe(false);
|
||||||
|
expect(isValidE164("+1 4155551212")).toBe(false);
|
||||||
|
expect(isValidE164("abc12345678")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects the empty string", () => {
|
||||||
|
expect(isValidE164("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("phoneVariants", () => {
|
||||||
|
it("returns an empty list for empty input", () => {
|
||||||
|
expect(phoneVariants("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always lists the original number first", () => {
|
||||||
|
const out = phoneVariants("37063949836");
|
||||||
|
expect(out[0]).toBe("37063949836");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inserts a trunk 0 after each plausible country-code length", () => {
|
||||||
|
// Input "37063949836" — CC-1 → "3" + "0" + "7063949836",
|
||||||
|
// CC-3 → "370" + "0" + "63949836".
|
||||||
|
// CC-2 is skipped because "063949836" already starts with 0.
|
||||||
|
const out = phoneVariants("37063949836");
|
||||||
|
expect(out).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"37063949836",
|
||||||
|
"307063949836",
|
||||||
|
"370063949836",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes a leading 0 after the country code when present", () => {
|
||||||
|
// Input "370063949836" — CC-2 strips one leading 0 from
|
||||||
|
// "0063949836" → "37" + "063949836" = "37063949836". Only one zero
|
||||||
|
// comes off per pass; that's what the live retry loop needs.
|
||||||
|
const out = phoneVariants("370063949836");
|
||||||
|
expect(out).toContain("370063949836");
|
||||||
|
expect(out).toContain("37063949836");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deduplicates variants that collapse to the same digits", () => {
|
||||||
|
const out = phoneVariants("37063949836");
|
||||||
|
expect(new Set(out).size).toBe(out.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns just the original when the number is too short for any CC slice", () => {
|
||||||
|
// 1-char input is shorter than all ccLen values; both loops skip.
|
||||||
|
expect(phoneVariants("1")).toEqual(["1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isRecipientNotAllowedError", () => {
|
||||||
|
it("matches Meta error code 131030", () => {
|
||||||
|
expect(
|
||||||
|
isRecipientNotAllowedError(
|
||||||
|
"(#131030) Recipient phone number not in allowed list",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the human-readable English variants", () => {
|
||||||
|
expect(isRecipientNotAllowedError("not in allowed list")).toBe(true);
|
||||||
|
expect(isRecipientNotAllowedError("recipient not in the allowed list")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Case-insensitive on the human text.
|
||||||
|
expect(isRecipientNotAllowedError("NOT IN ALLOWED LIST")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not false-positive on unrelated Meta errors", () => {
|
||||||
|
expect(isRecipientNotAllowedError("(#100) Invalid parameter")).toBe(false);
|
||||||
|
expect(isRecipientNotAllowedError("template name does not exist")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isRecipientNotAllowedError("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/lib/whatsapp/webhook-signature.test.ts
Normal file
69
src/lib/whatsapp/webhook-signature.test.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { verifyMetaWebhookSignature } from "./webhook-signature";
|
||||||
|
|
||||||
|
const SECRET = process.env.META_APP_SECRET!;
|
||||||
|
|
||||||
|
function signedHeader(body: string, secret: string = SECRET): string {
|
||||||
|
const hex = crypto.createHmac("sha256", secret).update(body).digest("hex");
|
||||||
|
return `sha256=${hex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("verifyMetaWebhookSignature", () => {
|
||||||
|
it("accepts a request signed with the correct secret", () => {
|
||||||
|
const body = JSON.stringify({ object: "whatsapp_business_account" });
|
||||||
|
expect(verifyMetaWebhookSignature(body, signedHeader(body))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a signature computed with a different secret", () => {
|
||||||
|
const body = "{}";
|
||||||
|
expect(verifyMetaWebhookSignature(body, signedHeader(body, "wrong"))).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects when the body has been tampered with after signing", () => {
|
||||||
|
const original = '{"entry":[]}';
|
||||||
|
const header = signedHeader(original);
|
||||||
|
const tampered = '{"entry":[{"id":"injected"}]}';
|
||||||
|
expect(verifyMetaWebhookSignature(tampered, header)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing header", () => {
|
||||||
|
expect(verifyMetaWebhookSignature("anything", null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a header without the sha256= prefix", () => {
|
||||||
|
const body = "{}";
|
||||||
|
const hex = crypto
|
||||||
|
.createHmac("sha256", SECRET)
|
||||||
|
.update(body)
|
||||||
|
.digest("hex");
|
||||||
|
expect(verifyMetaWebhookSignature(body, hex)).toBe(false);
|
||||||
|
expect(verifyMetaWebhookSignature(body, `sha512=${hex}`)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a header of the wrong length without throwing", () => {
|
||||||
|
// timingSafeEqual would throw on length mismatch — the guard inside
|
||||||
|
// the verifier should catch this and return false instead.
|
||||||
|
expect(verifyMetaWebhookSignature("{}", "sha256=tooshort")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fail-closed when secret is missing", () => {
|
||||||
|
const originalSecret = process.env.META_APP_SECRET;
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.META_APP_SECRET;
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.META_APP_SECRET = originalSecret;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects even a correctly-formed signature when no secret is configured", () => {
|
||||||
|
const body = "{}";
|
||||||
|
// Use the original secret to produce the header so we can verify
|
||||||
|
// the rejection is solely due to missing config.
|
||||||
|
const header = signedHeader(body, originalSecret!);
|
||||||
|
expect(verifyMetaWebhookSignature(body, header)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
21
vitest.config.ts
Normal file
21
vitest.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||||
|
// Dummy secrets — encryption.ts / webhook-signature.ts read these
|
||||||
|
// at module load. Tests never hit a real Meta/Supabase service, so
|
||||||
|
// any 32-byte hex / non-empty string will do; keep them lexically
|
||||||
|
// identical to the CI build env so behaviour matches.
|
||||||
|
env: {
|
||||||
|
ENCRYPTION_KEY:
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
META_APP_SECRET: "test-meta-app-secret",
|
||||||
|
},
|
||||||
|
clearMocks: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user