A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

feat: Add CI workflow and test coverage

Skywatch 33f7404f 90043cd7

+1511 -61
+45
.github/workflows/ci.yml
··· 1 + name: CI 2 + 3 + on: 4 + push: 5 + branches: [main] 6 + pull_request: 7 + branches: [main] 8 + 9 + jobs: 10 + test: 11 + name: Test 12 + runs-on: ubuntu-latest 13 + 14 + steps: 15 + - name: Checkout code 16 + uses: actions/checkout@v4 17 + 18 + - name: Setup Node.js 19 + uses: actions/setup-node@v4 20 + with: 21 + node-version: '20' 22 + cache: 'npm' 23 + 24 + - name: Install dependencies 25 + run: npm ci 26 + 27 + - name: Run linter 28 + run: npm run lint 29 + 30 + - name: Type check 31 + run: npx tsc --noEmit 32 + 33 + - name: Run tests 34 + run: npm run test:run 35 + 36 + - name: Generate coverage report 37 + run: npm run test:coverage 38 + 39 + - name: Upload coverage to Codecov 40 + uses: codecov/codecov-action@v4 41 + with: 42 + token: ${{ secrets.CODECOV_TOKEN }} 43 + files: ./coverage/coverage-final.json 44 + fail_ci_if_error: false 45 + verbose: true
+603 -61
package-lock.json
··· 37 37 "@types/eslint__js": "^8.42.3", 38 38 "@types/express": "^4.17.23", 39 39 "@types/node": "^22.18.0", 40 + "@vitest/coverage-v8": "^3.2.4", 40 41 "@vitest/ui": "^3.2.4", 41 42 "eslint": "^9.34.0", 42 43 "prettier": "^3.6.2", ··· 44 45 "typescript": "^5.9.2", 45 46 "typescript-eslint": "^8.42.0", 46 47 "vitest": "^3.2.4" 48 + } 49 + }, 50 + "node_modules/@ampproject/remapping": { 51 + "version": "2.3.0", 52 + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", 53 + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 54 + "dev": true, 55 + "license": "Apache-2.0", 56 + "dependencies": { 57 + "@jridgewell/gen-mapping": "^0.3.5", 58 + "@jridgewell/trace-mapping": "^0.3.24" 59 + }, 60 + "engines": { 61 + "node": ">=6.0.0" 47 62 } 48 63 }, 49 64 "node_modules/@atcute/atproto": { ··· 1235 1250 } 1236 1251 }, 1237 1252 "node_modules/@babel/helper-string-parser": { 1238 - "version": "7.25.9", 1253 + "version": "7.27.1", 1254 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 1255 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 1239 1256 "dev": true, 1240 1257 "license": "MIT", 1241 1258 "engines": { ··· 1243 1260 } 1244 1261 }, 1245 1262 "node_modules/@babel/helper-validator-identifier": { 1246 - "version": "7.25.9", 1263 + "version": "7.27.1", 1264 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 1265 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 1247 1266 "dev": true, 1248 1267 "license": "MIT", 1249 1268 "engines": { ··· 1377 1396 }, 1378 1397 "engines": { 1379 1398 "node": ">=6.9.0" 1399 + } 1400 + }, 1401 + "node_modules/@bcoe/v8-coverage": { 1402 + "version": "1.0.2", 1403 + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", 1404 + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", 1405 + "dev": true, 1406 + "license": "MIT", 1407 + "engines": { 1408 + "node": ">=18" 1380 1409 } 1381 1410 }, 1382 1411 "node_modules/@bufbuild/protobuf": { ··· 1928 1957 "multiformats": "^9.5.4" 1929 1958 } 1930 1959 }, 1960 + "node_modules/@isaacs/cliui": { 1961 + "version": "8.0.2", 1962 + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 1963 + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 1964 + "dev": true, 1965 + "license": "ISC", 1966 + "dependencies": { 1967 + "string-width": "^5.1.2", 1968 + "string-width-cjs": "npm:string-width@^4.2.0", 1969 + "strip-ansi": "^7.0.1", 1970 + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 1971 + "wrap-ansi": "^8.1.0", 1972 + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 1973 + }, 1974 + "engines": { 1975 + "node": ">=12" 1976 + } 1977 + }, 1978 + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { 1979 + "version": "6.2.3", 1980 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", 1981 + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", 1982 + "dev": true, 1983 + "license": "MIT", 1984 + "engines": { 1985 + "node": ">=12" 1986 + }, 1987 + "funding": { 1988 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1989 + } 1990 + }, 1991 + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { 1992 + "version": "9.2.2", 1993 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 1994 + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 1995 + "dev": true, 1996 + "license": "MIT" 1997 + }, 1998 + "node_modules/@isaacs/cliui/node_modules/string-width": { 1999 + "version": "5.1.2", 2000 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 2001 + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 2002 + "dev": true, 2003 + "license": "MIT", 2004 + "dependencies": { 2005 + "eastasianwidth": "^0.2.0", 2006 + "emoji-regex": "^9.2.2", 2007 + "strip-ansi": "^7.0.1" 2008 + }, 2009 + "engines": { 2010 + "node": ">=12" 2011 + }, 2012 + "funding": { 2013 + "url": "https://github.com/sponsors/sindresorhus" 2014 + } 2015 + }, 2016 + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { 2017 + "version": "8.1.0", 2018 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 2019 + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 2020 + "dev": true, 2021 + "license": "MIT", 2022 + "dependencies": { 2023 + "ansi-styles": "^6.1.0", 2024 + "string-width": "^5.0.1", 2025 + "strip-ansi": "^7.0.1" 2026 + }, 2027 + "engines": { 2028 + "node": ">=12" 2029 + }, 2030 + "funding": { 2031 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 2032 + } 2033 + }, 2034 + "node_modules/@istanbuljs/schema": { 2035 + "version": "0.1.3", 2036 + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", 2037 + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", 2038 + "dev": true, 2039 + "license": "MIT", 2040 + "engines": { 2041 + "node": ">=8" 2042 + } 2043 + }, 1931 2044 "node_modules/@jridgewell/gen-mapping": { 1932 2045 "version": "0.3.8", 1933 2046 "dev": true, ··· 1940 2053 "engines": { 1941 2054 "node": ">=6.0.0" 1942 2055 } 1943 - }, 1944 - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/sourcemap-codec": { 1945 - "version": "1.5.0", 1946 - "dev": true, 1947 - "license": "MIT" 1948 2056 }, 1949 2057 "node_modules/@jridgewell/resolve-uri": { 1950 2058 "version": "3.1.2", ··· 1963 2071 } 1964 2072 }, 1965 2073 "node_modules/@jridgewell/sourcemap-codec": { 1966 - "version": "1.5.0", 2074 + "version": "1.5.5", 2075 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 2076 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 1967 2077 "dev": true, 1968 2078 "license": "MIT" 1969 2079 }, 1970 2080 "node_modules/@jridgewell/trace-mapping": { 1971 - "version": "0.3.25", 2081 + "version": "0.3.31", 2082 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 2083 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 1972 2084 "dev": true, 1973 2085 "license": "MIT", 1974 2086 "dependencies": { 1975 2087 "@jridgewell/resolve-uri": "^3.1.0", 1976 2088 "@jridgewell/sourcemap-codec": "^1.4.14" 1977 2089 } 1978 - }, 1979 - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { 1980 - "version": "1.5.0", 1981 - "dev": true, 1982 - "license": "MIT" 1983 2090 }, 1984 2091 "node_modules/@js-sdsl/ordered-map": { 1985 2092 "version": "4.4.2", ··· 2084 2191 "license": "Apache-2.0", 2085 2192 "engines": { 2086 2193 "node": ">=8.0.0" 2194 + } 2195 + }, 2196 + "node_modules/@pkgjs/parseargs": { 2197 + "version": "0.11.0", 2198 + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 2199 + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 2200 + "dev": true, 2201 + "license": "MIT", 2202 + "optional": true, 2203 + "engines": { 2204 + "node": ">=14" 2087 2205 } 2088 2206 }, 2089 2207 "node_modules/@polka/url": { ··· 2524 2642 "typescript": ">=4.8.4 <6.0.0" 2525 2643 } 2526 2644 }, 2645 + "node_modules/@vitest/coverage-v8": { 2646 + "version": "3.2.4", 2647 + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", 2648 + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", 2649 + "dev": true, 2650 + "license": "MIT", 2651 + "dependencies": { 2652 + "@ampproject/remapping": "^2.3.0", 2653 + "@bcoe/v8-coverage": "^1.0.2", 2654 + "ast-v8-to-istanbul": "^0.3.3", 2655 + "debug": "^4.4.1", 2656 + "istanbul-lib-coverage": "^3.2.2", 2657 + "istanbul-lib-report": "^3.0.1", 2658 + "istanbul-lib-source-maps": "^5.0.6", 2659 + "istanbul-reports": "^3.1.7", 2660 + "magic-string": "^0.30.17", 2661 + "magicast": "^0.3.5", 2662 + "std-env": "^3.9.0", 2663 + "test-exclude": "^7.0.1", 2664 + "tinyrainbow": "^2.0.0" 2665 + }, 2666 + "funding": { 2667 + "url": "https://opencollective.com/vitest" 2668 + }, 2669 + "peerDependencies": { 2670 + "@vitest/browser": "3.2.4", 2671 + "vitest": "3.2.4" 2672 + }, 2673 + "peerDependenciesMeta": { 2674 + "@vitest/browser": { 2675 + "optional": true 2676 + } 2677 + } 2678 + }, 2527 2679 "node_modules/@vitest/expect": { 2528 2680 "version": "3.2.4", 2529 2681 "dev": true, ··· 2784 2936 "node": ">=12" 2785 2937 } 2786 2938 }, 2939 + "node_modules/ast-v8-to-istanbul": { 2940 + "version": "0.3.7", 2941 + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.7.tgz", 2942 + "integrity": "sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==", 2943 + "dev": true, 2944 + "license": "MIT", 2945 + "dependencies": { 2946 + "@jridgewell/trace-mapping": "^0.3.31", 2947 + "estree-walker": "^3.0.3", 2948 + "js-tokens": "^9.0.1" 2949 + } 2950 + }, 2951 + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { 2952 + "version": "9.0.1", 2953 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 2954 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 2955 + "dev": true, 2956 + "license": "MIT" 2957 + }, 2787 2958 "node_modules/asynckit": { 2788 2959 "version": "0.4.0", 2789 2960 "license": "MIT" ··· 3111 3282 "version": "8.0.0", 3112 3283 "license": "MIT" 3113 3284 }, 3114 - "node_modules/cliui/node_modules/string-width/node_modules/is-fullwidth-code-point": { 3115 - "version": "3.0.0", 3116 - "license": "MIT", 3117 - "engines": { 3118 - "node": ">=8" 3119 - } 3120 - }, 3121 3285 "node_modules/cliui/node_modules/strip-ansi": { 3122 3286 "version": "6.0.1", 3123 3287 "license": "MIT", ··· 3320 3484 } 3321 3485 }, 3322 3486 "node_modules/debug": { 3323 - "version": "4.4.0", 3487 + "version": "4.4.3", 3488 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 3489 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 3324 3490 "license": "MIT", 3325 3491 "dependencies": { 3326 3492 "ms": "^2.1.3" ··· 3447 3613 "engines": { 3448 3614 "node": ">= 6" 3449 3615 } 3616 + }, 3617 + "node_modules/eastasianwidth": { 3618 + "version": "0.2.0", 3619 + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 3620 + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 3621 + "dev": true, 3622 + "license": "MIT" 3450 3623 }, 3451 3624 "node_modules/ee-first": { 3452 3625 "version": "1.1.1", ··· 4226 4399 } 4227 4400 } 4228 4401 }, 4402 + "node_modules/foreground-child": { 4403 + "version": "3.3.1", 4404 + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", 4405 + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 4406 + "dev": true, 4407 + "license": "ISC", 4408 + "dependencies": { 4409 + "cross-spawn": "^7.0.6", 4410 + "signal-exit": "^4.0.1" 4411 + }, 4412 + "engines": { 4413 + "node": ">=14" 4414 + }, 4415 + "funding": { 4416 + "url": "https://github.com/sponsors/isaacs" 4417 + } 4418 + }, 4229 4419 "node_modules/form-data": { 4230 4420 "version": "4.0.1", 4231 4421 "license": "MIT", ··· 4320 4510 "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 4321 4511 } 4322 4512 }, 4513 + "node_modules/glob": { 4514 + "version": "10.4.5", 4515 + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 4516 + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 4517 + "dev": true, 4518 + "license": "ISC", 4519 + "dependencies": { 4520 + "foreground-child": "^3.1.0", 4521 + "jackspeak": "^3.1.2", 4522 + "minimatch": "^9.0.4", 4523 + "minipass": "^7.1.2", 4524 + "package-json-from-dist": "^1.0.0", 4525 + "path-scurry": "^1.11.1" 4526 + }, 4527 + "bin": { 4528 + "glob": "dist/esm/bin.mjs" 4529 + }, 4530 + "funding": { 4531 + "url": "https://github.com/sponsors/isaacs" 4532 + } 4533 + }, 4323 4534 "node_modules/glob-parent": { 4324 4535 "version": "6.0.2", 4325 4536 "dev": true, ··· 4329 4540 }, 4330 4541 "engines": { 4331 4542 "node": ">=10.13.0" 4543 + } 4544 + }, 4545 + "node_modules/glob/node_modules/brace-expansion": { 4546 + "version": "2.0.2", 4547 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 4548 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 4549 + "dev": true, 4550 + "license": "MIT", 4551 + "dependencies": { 4552 + "balanced-match": "^1.0.0" 4553 + } 4554 + }, 4555 + "node_modules/glob/node_modules/minimatch": { 4556 + "version": "9.0.5", 4557 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 4558 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 4559 + "dev": true, 4560 + "license": "ISC", 4561 + "dependencies": { 4562 + "brace-expansion": "^2.0.1" 4563 + }, 4564 + "engines": { 4565 + "node": ">=16 || 14 >=14.17" 4566 + }, 4567 + "funding": { 4568 + "url": "https://github.com/sponsors/isaacs" 4332 4569 } 4333 4570 }, 4334 4571 "node_modules/gopd": { ··· 4393 4630 "minimalistic-assert": "^1.0.0", 4394 4631 "minimalistic-crypto-utils": "^1.0.1" 4395 4632 } 4633 + }, 4634 + "node_modules/html-escaper": { 4635 + "version": "2.0.2", 4636 + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", 4637 + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", 4638 + "dev": true, 4639 + "license": "MIT" 4396 4640 }, 4397 4641 "node_modules/http-errors": { 4398 4642 "version": "2.0.0", ··· 4549 4793 "node": ">=0.10.0" 4550 4794 } 4551 4795 }, 4796 + "node_modules/is-fullwidth-code-point": { 4797 + "version": "3.0.0", 4798 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 4799 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 4800 + "license": "MIT", 4801 + "engines": { 4802 + "node": ">=8" 4803 + } 4804 + }, 4552 4805 "node_modules/is-glob": { 4553 4806 "version": "4.0.3", 4554 4807 "dev": true, ··· 4585 4838 "version": "2.2.2", 4586 4839 "license": "MIT" 4587 4840 }, 4841 + "node_modules/istanbul-lib-coverage": { 4842 + "version": "3.2.2", 4843 + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", 4844 + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", 4845 + "dev": true, 4846 + "license": "BSD-3-Clause", 4847 + "engines": { 4848 + "node": ">=8" 4849 + } 4850 + }, 4851 + "node_modules/istanbul-lib-report": { 4852 + "version": "3.0.1", 4853 + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", 4854 + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", 4855 + "dev": true, 4856 + "license": "BSD-3-Clause", 4857 + "dependencies": { 4858 + "istanbul-lib-coverage": "^3.0.0", 4859 + "make-dir": "^4.0.0", 4860 + "supports-color": "^7.1.0" 4861 + }, 4862 + "engines": { 4863 + "node": ">=10" 4864 + } 4865 + }, 4866 + "node_modules/istanbul-lib-source-maps": { 4867 + "version": "5.0.6", 4868 + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", 4869 + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", 4870 + "dev": true, 4871 + "license": "BSD-3-Clause", 4872 + "dependencies": { 4873 + "@jridgewell/trace-mapping": "^0.3.23", 4874 + "debug": "^4.1.1", 4875 + "istanbul-lib-coverage": "^3.0.0" 4876 + }, 4877 + "engines": { 4878 + "node": ">=10" 4879 + } 4880 + }, 4881 + "node_modules/istanbul-reports": { 4882 + "version": "3.2.0", 4883 + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", 4884 + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", 4885 + "dev": true, 4886 + "license": "BSD-3-Clause", 4887 + "dependencies": { 4888 + "html-escaper": "^2.0.0", 4889 + "istanbul-lib-report": "^3.0.0" 4890 + }, 4891 + "engines": { 4892 + "node": ">=8" 4893 + } 4894 + }, 4895 + "node_modules/jackspeak": { 4896 + "version": "3.4.3", 4897 + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 4898 + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 4899 + "dev": true, 4900 + "license": "BlueOak-1.0.0", 4901 + "dependencies": { 4902 + "@isaacs/cliui": "^8.0.2" 4903 + }, 4904 + "funding": { 4905 + "url": "https://github.com/sponsors/isaacs" 4906 + }, 4907 + "optionalDependencies": { 4908 + "@pkgjs/parseargs": "^0.11.0" 4909 + } 4910 + }, 4588 4911 "node_modules/javascript-natural-sort": { 4589 4912 "version": "0.7.1", 4590 4913 "dev": true, ··· 4906 5229 "dev": true, 4907 5230 "license": "MIT" 4908 5231 }, 5232 + "node_modules/lru-cache": { 5233 + "version": "10.4.3", 5234 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 5235 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 5236 + "dev": true, 5237 + "license": "ISC" 5238 + }, 4909 5239 "node_modules/magic-string": { 4910 5240 "version": "0.30.19", 4911 5241 "dev": true, ··· 4914 5244 "@jridgewell/sourcemap-codec": "^1.5.5" 4915 5245 } 4916 5246 }, 5247 + "node_modules/magicast": { 5248 + "version": "0.3.5", 5249 + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", 5250 + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", 5251 + "dev": true, 5252 + "license": "MIT", 5253 + "dependencies": { 5254 + "@babel/parser": "^7.25.4", 5255 + "@babel/types": "^7.25.4", 5256 + "source-map-js": "^1.2.0" 5257 + } 5258 + }, 5259 + "node_modules/magicast/node_modules/@babel/types": { 5260 + "version": "7.28.4", 5261 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", 5262 + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", 5263 + "dev": true, 5264 + "license": "MIT", 5265 + "dependencies": { 5266 + "@babel/helper-string-parser": "^7.27.1", 5267 + "@babel/helper-validator-identifier": "^7.27.1" 5268 + }, 5269 + "engines": { 5270 + "node": ">=6.9.0" 5271 + } 5272 + }, 5273 + "node_modules/make-dir": { 5274 + "version": "4.0.0", 5275 + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", 5276 + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", 5277 + "dev": true, 5278 + "license": "MIT", 5279 + "dependencies": { 5280 + "semver": "^7.5.3" 5281 + }, 5282 + "engines": { 5283 + "node": ">=10" 5284 + }, 5285 + "funding": { 5286 + "url": "https://github.com/sponsors/sindresorhus" 5287 + } 5288 + }, 4917 5289 "node_modules/math-intrinsics": { 4918 5290 "version": "1.1.0", 4919 5291 "license": "MIT", ··· 5046 5418 "license": "MIT", 5047 5419 "funding": { 5048 5420 "url": "https://github.com/sponsors/ljharb" 5421 + } 5422 + }, 5423 + "node_modules/minipass": { 5424 + "version": "7.1.2", 5425 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 5426 + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 5427 + "dev": true, 5428 + "license": "ISC", 5429 + "engines": { 5430 + "node": ">=16 || 14 >=14.17" 5049 5431 } 5050 5432 }, 5051 5433 "node_modules/module-alias": { ··· 5313 5695 "url": "https://github.com/sponsors/sindresorhus" 5314 5696 } 5315 5697 }, 5698 + "node_modules/package-json-from-dist": { 5699 + "version": "1.0.1", 5700 + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 5701 + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 5702 + "dev": true, 5703 + "license": "BlueOak-1.0.0" 5704 + }, 5316 5705 "node_modules/parent-module": { 5317 5706 "version": "1.0.1", 5318 5707 "dev": true, ··· 5351 5740 "license": "MIT", 5352 5741 "engines": { 5353 5742 "node": ">=8" 5743 + } 5744 + }, 5745 + "node_modules/path-scurry": { 5746 + "version": "1.11.1", 5747 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 5748 + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 5749 + "dev": true, 5750 + "license": "BlueOak-1.0.0", 5751 + "dependencies": { 5752 + "lru-cache": "^10.2.0", 5753 + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 5754 + }, 5755 + "engines": { 5756 + "node": ">=16 || 14 >=14.18" 5757 + }, 5758 + "funding": { 5759 + "url": "https://github.com/sponsors/isaacs" 5354 5760 } 5355 5761 }, 5356 5762 "node_modules/path-to-regexp": { ··· 6614 7020 "url": "https://github.com/sponsors/sindresorhus" 6615 7021 } 6616 7022 }, 7023 + "node_modules/string-width-cjs": { 7024 + "name": "string-width", 7025 + "version": "4.2.3", 7026 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 7027 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 7028 + "dev": true, 7029 + "license": "MIT", 7030 + "dependencies": { 7031 + "emoji-regex": "^8.0.0", 7032 + "is-fullwidth-code-point": "^3.0.0", 7033 + "strip-ansi": "^6.0.1" 7034 + }, 7035 + "engines": { 7036 + "node": ">=8" 7037 + } 7038 + }, 7039 + "node_modules/string-width-cjs/node_modules/ansi-regex": { 7040 + "version": "5.0.1", 7041 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 7042 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 7043 + "dev": true, 7044 + "license": "MIT", 7045 + "engines": { 7046 + "node": ">=8" 7047 + } 7048 + }, 7049 + "node_modules/string-width-cjs/node_modules/emoji-regex": { 7050 + "version": "8.0.0", 7051 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 7052 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 7053 + "dev": true, 7054 + "license": "MIT" 7055 + }, 7056 + "node_modules/string-width-cjs/node_modules/strip-ansi": { 7057 + "version": "6.0.1", 7058 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 7059 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 7060 + "dev": true, 7061 + "license": "MIT", 7062 + "dependencies": { 7063 + "ansi-regex": "^5.0.1" 7064 + }, 7065 + "engines": { 7066 + "node": ">=8" 7067 + } 7068 + }, 6617 7069 "node_modules/strip-ansi": { 6618 7070 "version": "7.1.0", 6619 7071 "license": "MIT", ··· 6627 7079 "url": "https://github.com/chalk/strip-ansi?sponsor=1" 6628 7080 } 6629 7081 }, 7082 + "node_modules/strip-ansi-cjs": { 7083 + "name": "strip-ansi", 7084 + "version": "6.0.1", 7085 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 7086 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 7087 + "dev": true, 7088 + "license": "MIT", 7089 + "dependencies": { 7090 + "ansi-regex": "^5.0.1" 7091 + }, 7092 + "engines": { 7093 + "node": ">=8" 7094 + } 7095 + }, 7096 + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 7097 + "version": "5.0.1", 7098 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 7099 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 7100 + "dev": true, 7101 + "license": "MIT", 7102 + "engines": { 7103 + "node": ">=8" 7104 + } 7105 + }, 6630 7106 "node_modules/strip-final-newline": { 6631 7107 "version": "3.0.0", 6632 7108 "license": "MIT", ··· 6677 7153 "license": "MIT", 6678 7154 "dependencies": { 6679 7155 "bintrees": "1.0.2" 7156 + } 7157 + }, 7158 + "node_modules/test-exclude": { 7159 + "version": "7.0.1", 7160 + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", 7161 + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", 7162 + "dev": true, 7163 + "license": "ISC", 7164 + "dependencies": { 7165 + "@istanbuljs/schema": "^0.1.2", 7166 + "glob": "^10.4.1", 7167 + "minimatch": "^9.0.4" 7168 + }, 7169 + "engines": { 7170 + "node": ">=18" 7171 + } 7172 + }, 7173 + "node_modules/test-exclude/node_modules/brace-expansion": { 7174 + "version": "2.0.2", 7175 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 7176 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 7177 + "dev": true, 7178 + "license": "MIT", 7179 + "dependencies": { 7180 + "balanced-match": "^1.0.0" 7181 + } 7182 + }, 7183 + "node_modules/test-exclude/node_modules/minimatch": { 7184 + "version": "9.0.5", 7185 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 7186 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 7187 + "dev": true, 7188 + "license": "ISC", 7189 + "dependencies": { 7190 + "brace-expansion": "^2.0.1" 7191 + }, 7192 + "engines": { 7193 + "node": ">=16 || 14 >=14.17" 7194 + }, 7195 + "funding": { 7196 + "url": "https://github.com/sponsors/isaacs" 6680 7197 } 6681 7198 }, 6682 7199 "node_modules/thread-stream": { ··· 7468 7985 "url": "https://opencollective.com/vitest" 7469 7986 } 7470 7987 }, 7471 - "node_modules/vite-node/node_modules/debug": { 7472 - "version": "4.4.3", 7473 - "dev": true, 7474 - "license": "MIT", 7475 - "dependencies": { 7476 - "ms": "^2.1.3" 7477 - }, 7478 - "engines": { 7479 - "node": ">=6.0" 7480 - }, 7481 - "peerDependenciesMeta": { 7482 - "supports-color": { 7483 - "optional": true 7484 - } 7485 - } 7486 - }, 7487 7988 "node_modules/vitest": { 7488 7989 "version": "3.2.4", 7489 7990 "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", ··· 7557 8058 } 7558 8059 } 7559 8060 }, 7560 - "node_modules/vitest/node_modules/debug": { 7561 - "version": "4.4.3", 7562 - "dev": true, 7563 - "license": "MIT", 7564 - "dependencies": { 7565 - "ms": "^2.1.3" 7566 - }, 7567 - "engines": { 7568 - "node": ">=6.0" 7569 - }, 7570 - "peerDependenciesMeta": { 7571 - "supports-color": { 7572 - "optional": true 7573 - } 7574 - } 7575 - }, 7576 8061 "node_modules/webidl-conversions": { 7577 8062 "version": "3.0.1", 7578 8063 "license": "BSD-2-Clause" ··· 7636 8121 "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 7637 8122 } 7638 8123 }, 8124 + "node_modules/wrap-ansi-cjs": { 8125 + "name": "wrap-ansi", 8126 + "version": "7.0.0", 8127 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 8128 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 8129 + "dev": true, 8130 + "license": "MIT", 8131 + "dependencies": { 8132 + "ansi-styles": "^4.0.0", 8133 + "string-width": "^4.1.0", 8134 + "strip-ansi": "^6.0.0" 8135 + }, 8136 + "engines": { 8137 + "node": ">=10" 8138 + }, 8139 + "funding": { 8140 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 8141 + } 8142 + }, 8143 + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 8144 + "version": "5.0.1", 8145 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 8146 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 8147 + "dev": true, 8148 + "license": "MIT", 8149 + "engines": { 8150 + "node": ">=8" 8151 + } 8152 + }, 8153 + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 8154 + "version": "8.0.0", 8155 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 8156 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 8157 + "dev": true, 8158 + "license": "MIT" 8159 + }, 8160 + "node_modules/wrap-ansi-cjs/node_modules/string-width": { 8161 + "version": "4.2.3", 8162 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 8163 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 8164 + "dev": true, 8165 + "license": "MIT", 8166 + "dependencies": { 8167 + "emoji-regex": "^8.0.0", 8168 + "is-fullwidth-code-point": "^3.0.0", 8169 + "strip-ansi": "^6.0.1" 8170 + }, 8171 + "engines": { 8172 + "node": ">=8" 8173 + } 8174 + }, 8175 + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 8176 + "version": "6.0.1", 8177 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 8178 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 8179 + "dev": true, 8180 + "license": "MIT", 8181 + "dependencies": { 8182 + "ansi-regex": "^5.0.1" 8183 + }, 8184 + "engines": { 8185 + "node": ">=8" 8186 + } 8187 + }, 7639 8188 "node_modules/wrap-ansi/node_modules/ansi-styles": { 7640 8189 "version": "6.2.1", 7641 8190 "license": "MIT", ··· 7731 8280 "node_modules/yargs/node_modules/string-width/node_modules/emoji-regex": { 7732 8281 "version": "8.0.0", 7733 8282 "license": "MIT" 7734 - }, 7735 - "node_modules/yargs/node_modules/string-width/node_modules/is-fullwidth-code-point": { 7736 - "version": "3.0.0", 7737 - "license": "MIT", 7738 - "engines": { 7739 - "node": ">=8" 7740 - } 7741 8283 }, 7742 8284 "node_modules/yargs/node_modules/string-width/node_modules/strip-ansi": { 7743 8285 "version": "6.0.1",
+1
package.json
··· 24 24 "@types/eslint__js": "^8.42.3", 25 25 "@types/express": "^4.17.23", 26 26 "@types/node": "^22.18.0", 27 + "@vitest/coverage-v8": "^3.2.4", 27 28 "@vitest/ui": "^3.2.4", 28 29 "eslint": "^9.34.0", 29 30 "prettier": "^3.6.2",
+332
src/rules/handles/checkHandles.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { checkHandle } from "./checkHandles.js"; 3 + import { 4 + createAccountReport, 5 + createAccountComment, 6 + createAccountLabel, 7 + } from "../../moderation.js"; 8 + 9 + // Mock dependencies 10 + vi.mock("../../moderation.js", () => ({ 11 + createAccountReport: vi.fn(), 12 + createAccountComment: vi.fn(), 13 + createAccountLabel: vi.fn(), 14 + })); 15 + 16 + vi.mock("../../logger.js", () => ({ 17 + logger: { 18 + debug: vi.fn(), 19 + info: vi.fn(), 20 + warn: vi.fn(), 21 + }, 22 + })); 23 + 24 + vi.mock("../../constants.js", () => ({ 25 + GLOBAL_ALLOW: ["did:plc:globalallow"], 26 + })); 27 + 28 + // Mock HANDLE_CHECKS with various test scenarios 29 + vi.mock("./constants.js", () => ({ 30 + HANDLE_CHECKS: [ 31 + { 32 + label: "spam", 33 + comment: "Spam detected", 34 + reportAcct: true, 35 + commentAcct: false, 36 + toLabel: false, 37 + check: /spam/i, 38 + }, 39 + { 40 + label: "scam", 41 + comment: "Scam detected", 42 + reportAcct: false, 43 + commentAcct: true, 44 + toLabel: false, 45 + check: /scam/i, 46 + whitelist: /legit-scam/i, 47 + }, 48 + { 49 + label: "bot", 50 + comment: "Bot detected", 51 + reportAcct: false, 52 + commentAcct: false, 53 + toLabel: true, 54 + check: /bot-\d+/i, 55 + ignoredDIDs: ["did:plc:ignoredbot"], 56 + }, 57 + { 58 + label: "multi-action", 59 + comment: "Multi-action triggered", 60 + reportAcct: true, 61 + commentAcct: true, 62 + toLabel: true, 63 + check: /dangerous/i, 64 + }, 65 + { 66 + label: "whitelist-test", 67 + comment: "Whitelisted pattern", 68 + reportAcct: true, 69 + commentAcct: false, 70 + toLabel: false, 71 + check: /test/i, 72 + whitelist: /good-test/i, 73 + ignoredDIDs: ["did:plc:testuser"], 74 + }, 75 + ], 76 + })); 77 + 78 + describe("checkHandle", () => { 79 + beforeEach(() => { 80 + vi.clearAllMocks(); 81 + }); 82 + 83 + describe("global allow list", () => { 84 + it("should skip checks for globally allowed DIDs", async () => { 85 + await checkHandle("did:plc:globalallow", "spam-account", Date.now()); 86 + 87 + expect(createAccountReport).not.toHaveBeenCalled(); 88 + expect(createAccountComment).not.toHaveBeenCalled(); 89 + expect(createAccountLabel).not.toHaveBeenCalled(); 90 + }); 91 + 92 + it("should process non-globally-allowed DIDs", async () => { 93 + await checkHandle("did:plc:normal", "spam-account", Date.now()); 94 + 95 + expect(createAccountReport).toHaveBeenCalled(); 96 + }); 97 + }); 98 + 99 + describe("pattern matching", () => { 100 + it("should trigger on matching pattern", async () => { 101 + const time = Date.now(); 102 + await checkHandle("did:plc:user1", "spam-account", time); 103 + 104 + expect(createAccountReport).toHaveBeenCalledWith( 105 + "did:plc:user1", 106 + `${time}: Spam detected - spam-account`, 107 + ); 108 + }); 109 + 110 + it("should not trigger on non-matching pattern", async () => { 111 + await checkHandle("did:plc:user1", "normal-account", Date.now()); 112 + 113 + expect(createAccountReport).not.toHaveBeenCalled(); 114 + expect(createAccountComment).not.toHaveBeenCalled(); 115 + expect(createAccountLabel).not.toHaveBeenCalled(); 116 + }); 117 + 118 + it("should be case insensitive", async () => { 119 + const time = Date.now(); 120 + await checkHandle("did:plc:user1", "SPAM-ACCOUNT", time); 121 + 122 + expect(createAccountReport).toHaveBeenCalledWith( 123 + "did:plc:user1", 124 + `${time}: Spam detected - SPAM-ACCOUNT`, 125 + ); 126 + }); 127 + }); 128 + 129 + describe("whitelist handling", () => { 130 + it("should not trigger on whitelisted pattern", async () => { 131 + await checkHandle("did:plc:user1", "legit-scam-detector", Date.now()); 132 + 133 + expect(createAccountComment).not.toHaveBeenCalled(); 134 + }); 135 + 136 + it("should trigger on non-whitelisted match", async () => { 137 + const time = Date.now(); 138 + await checkHandle("did:plc:user1", "scam-account", time); 139 + 140 + expect(createAccountComment).toHaveBeenCalledWith( 141 + "did:plc:user1", 142 + `${time}: Scam detected - scam-account`, 143 + ); 144 + }); 145 + }); 146 + 147 + describe("ignored DIDs", () => { 148 + it("should skip checks for ignored DIDs in specific rules", async () => { 149 + await checkHandle("did:plc:ignoredbot", "bot-123", Date.now()); 150 + 151 + expect(createAccountLabel).not.toHaveBeenCalled(); 152 + }); 153 + 154 + it("should process non-ignored DIDs for specific rules", async () => { 155 + const time = Date.now(); 156 + await checkHandle("did:plc:normaluser", "bot-456", time); 157 + 158 + expect(createAccountLabel).toHaveBeenCalledWith( 159 + "did:plc:normaluser", 160 + "bot", 161 + `${time}: Bot detected - bot-456`, 162 + ); 163 + }); 164 + }); 165 + 166 + describe("action types", () => { 167 + it("should create report when reportAcct is true", async () => { 168 + const time = Date.now(); 169 + await checkHandle("did:plc:user1", "spam-user", time); 170 + 171 + expect(createAccountReport).toHaveBeenCalledWith( 172 + "did:plc:user1", 173 + `${time}: Spam detected - spam-user`, 174 + ); 175 + }); 176 + 177 + it("should create comment when commentAcct is true", async () => { 178 + const time = Date.now(); 179 + await checkHandle("did:plc:user1", "scam-user", time); 180 + 181 + expect(createAccountComment).toHaveBeenCalledWith( 182 + "did:plc:user1", 183 + `${time}: Scam detected - scam-user`, 184 + ); 185 + }); 186 + 187 + it("should create label when toLabel is true", async () => { 188 + const time = Date.now(); 189 + await checkHandle("did:plc:user1", "bot-789", time); 190 + 191 + expect(createAccountLabel).toHaveBeenCalledWith( 192 + "did:plc:user1", 193 + "bot", 194 + `${time}: Bot detected - bot-789`, 195 + ); 196 + }); 197 + 198 + it("should perform multiple actions when configured", async () => { 199 + const time = Date.now(); 200 + await checkHandle("did:plc:user1", "dangerous-account", time); 201 + 202 + expect(createAccountReport).toHaveBeenCalledWith( 203 + "did:plc:user1", 204 + `${time}: Multi-action triggered - dangerous-account`, 205 + ); 206 + expect(createAccountComment).toHaveBeenCalledWith( 207 + "did:plc:user1", 208 + `${time}: Multi-action triggered - dangerous-account`, 209 + ); 210 + expect(createAccountLabel).toHaveBeenCalledWith( 211 + "did:plc:user1", 212 + "multi-action", 213 + `${time}: Multi-action triggered - dangerous-account`, 214 + ); 215 + }); 216 + }); 217 + 218 + describe("multiple rule matching", () => { 219 + it("should process all matching rules", async () => { 220 + vi.resetModules(); 221 + // Re-import with a mock that has overlapping patterns 222 + vi.doMock("./constants.js", () => ({ 223 + HANDLE_CHECKS: [ 224 + { 225 + label: "pattern1", 226 + comment: "Pattern 1", 227 + reportAcct: true, 228 + commentAcct: false, 229 + toLabel: false, 230 + check: /test/i, 231 + }, 232 + { 233 + label: "pattern2", 234 + comment: "Pattern 2", 235 + reportAcct: false, 236 + commentAcct: true, 237 + toLabel: false, 238 + check: /test/i, 239 + }, 240 + ], 241 + })); 242 + 243 + const { checkHandle: checkHandleReimport } = await import( 244 + "./checkHandles.js" 245 + ); 246 + const time = Date.now(); 247 + await checkHandleReimport("did:plc:user1", "test-account", time); 248 + 249 + expect(createAccountReport).toHaveBeenCalledTimes(1); 250 + expect(createAccountComment).toHaveBeenCalledTimes(1); 251 + }); 252 + }); 253 + 254 + describe("edge cases", () => { 255 + it("should handle empty handle strings", async () => { 256 + await checkHandle("did:plc:user1", "", Date.now()); 257 + 258 + expect(createAccountReport).not.toHaveBeenCalled(); 259 + expect(createAccountComment).not.toHaveBeenCalled(); 260 + expect(createAccountLabel).not.toHaveBeenCalled(); 261 + }); 262 + 263 + it("should handle special characters in handles", async () => { 264 + await checkHandle( 265 + "did:plc:user1", 266 + "spam-!@#$%^&*()", 267 + Date.now(), 268 + ); 269 + 270 + expect(createAccountReport).toHaveBeenCalled(); 271 + }); 272 + 273 + it("should handle very long handles", async () => { 274 + const longHandle = "spam-" + "a".repeat(1000); 275 + const time = Date.now(); 276 + await checkHandle("did:plc:user1", longHandle, time); 277 + 278 + expect(createAccountReport).toHaveBeenCalledWith( 279 + "did:plc:user1", 280 + `${time}: Spam detected - ${longHandle}`, 281 + ); 282 + }); 283 + 284 + it("should handle unicode characters in handles", async () => { 285 + await checkHandle("did:plc:user1", "spam-账户-🤖", Date.now()); 286 + 287 + expect(createAccountReport).toHaveBeenCalled(); 288 + }); 289 + }); 290 + 291 + describe("timestamp handling", () => { 292 + it("should include timestamp in action comments", async () => { 293 + const time = 1234567890; 294 + await checkHandle("did:plc:user1", "spam-account", time); 295 + 296 + expect(createAccountReport).toHaveBeenCalledWith( 297 + "did:plc:user1", 298 + "1234567890: Spam detected - spam-account", 299 + ); 300 + }); 301 + 302 + it("should handle different timestamp formats", async () => { 303 + const time = Date.now(); 304 + await checkHandle("did:plc:user1", "spam-account", time); 305 + 306 + expect(createAccountReport).toHaveBeenCalledWith( 307 + "did:plc:user1", 308 + expect.stringContaining(time.toString()), 309 + ); 310 + }); 311 + }); 312 + 313 + describe("whitelist and ignoredDIDs combination", () => { 314 + it("should respect both whitelist and ignoredDIDs", async () => { 315 + await checkHandle("did:plc:testuser", "good-test-account", Date.now()); 316 + 317 + expect(createAccountReport).not.toHaveBeenCalled(); 318 + }); 319 + 320 + it("should skip on ignoredDID even if not whitelisted", async () => { 321 + await checkHandle("did:plc:testuser", "bad-test-account", Date.now()); 322 + 323 + expect(createAccountReport).not.toHaveBeenCalled(); 324 + }); 325 + 326 + it("should skip on whitelist even if not in ignoredDIDs", async () => { 327 + await checkHandle("did:plc:otheruser", "good-test-account", Date.now()); 328 + 329 + expect(createAccountReport).not.toHaveBeenCalled(); 330 + }); 331 + }); 332 + });
+256
src/utils/getFinalUrl.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { getFinalUrl } from "./getFinalUrl.js"; 3 + 4 + // Mock the logger 5 + vi.mock("../logger.js", () => ({ 6 + logger: { 7 + debug: vi.fn(), 8 + warn: vi.fn(), 9 + }, 10 + })); 11 + 12 + describe("getFinalUrl", () => { 13 + beforeEach(() => { 14 + vi.clearAllMocks(); 15 + vi.unstubAllGlobals(); 16 + }); 17 + 18 + describe("successful HEAD requests", () => { 19 + it("should return the final URL after redirect", async () => { 20 + const mockFetch = vi.fn().mockResolvedValue({ 21 + url: "https://example.com/final", 22 + }); 23 + vi.stubGlobal("fetch", mockFetch); 24 + 25 + const result = await getFinalUrl("https://example.com/redirect"); 26 + 27 + expect(result).toBe("https://example.com/final"); 28 + expect(mockFetch).toHaveBeenCalledWith( 29 + "https://example.com/redirect", 30 + expect.objectContaining({ 31 + method: "HEAD", 32 + redirect: "follow", 33 + }), 34 + ); 35 + }); 36 + 37 + it("should return the same URL if no redirect", async () => { 38 + const mockFetch = vi.fn().mockResolvedValue({ 39 + url: "https://example.com/page", 40 + }); 41 + vi.stubGlobal("fetch", mockFetch); 42 + 43 + const result = await getFinalUrl("https://example.com/page"); 44 + 45 + expect(result).toBe("https://example.com/page"); 46 + }); 47 + 48 + it("should include proper user agent header", async () => { 49 + const mockFetch = vi.fn().mockResolvedValue({ 50 + url: "https://example.com/final", 51 + }); 52 + vi.stubGlobal("fetch", mockFetch); 53 + 54 + await getFinalUrl("https://example.com/test"); 55 + 56 + expect(mockFetch).toHaveBeenCalledWith( 57 + "https://example.com/test", 58 + expect.objectContaining({ 59 + headers: { 60 + "User-Agent": expect.stringContaining("SkyWatch"), 61 + }, 62 + }), 63 + ); 64 + }); 65 + }); 66 + 67 + describe("HEAD request failures with GET fallback", () => { 68 + it("should fallback to GET when HEAD fails", async () => { 69 + const mockFetch = vi 70 + .fn() 71 + .mockRejectedValueOnce(new Error("HEAD not allowed")) 72 + .mockResolvedValueOnce({ 73 + url: "https://example.com/final", 74 + }); 75 + vi.stubGlobal("fetch", mockFetch); 76 + 77 + const result = await getFinalUrl("https://example.com/test"); 78 + 79 + expect(result).toBe("https://example.com/final"); 80 + expect(mockFetch).toHaveBeenCalledTimes(2); 81 + expect(mockFetch).toHaveBeenNthCalledWith( 82 + 1, 83 + "https://example.com/test", 84 + expect.objectContaining({ method: "HEAD" }), 85 + ); 86 + expect(mockFetch).toHaveBeenNthCalledWith( 87 + 2, 88 + "https://example.com/test", 89 + expect.objectContaining({ method: "GET" }), 90 + ); 91 + }); 92 + 93 + it("should handle network errors on HEAD with GET success", async () => { 94 + const mockFetch = vi 95 + .fn() 96 + .mockRejectedValueOnce(new Error("Network error")) 97 + .mockResolvedValueOnce({ 98 + url: "https://example.com/final", 99 + }); 100 + vi.stubGlobal("fetch", mockFetch); 101 + 102 + const result = await getFinalUrl("https://example.com/test"); 103 + 104 + expect(result).toBe("https://example.com/final"); 105 + expect(mockFetch).toHaveBeenCalledTimes(2); 106 + }); 107 + }); 108 + 109 + describe("timeout handling", () => { 110 + it("should configure abort signal with timeout for HEAD request", async () => { 111 + const mockFetch = vi.fn().mockResolvedValue({ 112 + url: "https://example.com/final", 113 + }); 114 + vi.stubGlobal("fetch", mockFetch); 115 + 116 + await getFinalUrl("https://example.com/test"); 117 + 118 + expect(mockFetch).toHaveBeenCalledWith( 119 + "https://example.com/test", 120 + expect.objectContaining({ 121 + signal: expect.any(AbortSignal), 122 + }), 123 + ); 124 + }); 125 + 126 + it("should handle AbortError from timeout", async () => { 127 + const mockFetch = vi.fn().mockRejectedValue( 128 + Object.assign(new Error("The operation was aborted"), { 129 + name: "AbortError", 130 + }), 131 + ); 132 + vi.stubGlobal("fetch", mockFetch); 133 + 134 + await expect(getFinalUrl("https://slow.example.com")).rejects.toThrow( 135 + "The operation was aborted", 136 + ); 137 + }); 138 + }); 139 + 140 + describe("complete failure scenarios", () => { 141 + it("should throw error when both HEAD and GET fail", async () => { 142 + const mockFetch = vi.fn().mockRejectedValue(new Error("Network error")); 143 + vi.stubGlobal("fetch", mockFetch); 144 + 145 + await expect(getFinalUrl("https://example.com/test")).rejects.toThrow( 146 + "Network error", 147 + ); 148 + expect(mockFetch).toHaveBeenCalledTimes(2); 149 + }); 150 + 151 + it("should throw AbortError on timeout", async () => { 152 + const mockFetch = vi.fn().mockImplementation(() => { 153 + const error = new Error("The operation was aborted"); 154 + error.name = "AbortError"; 155 + return Promise.reject(error); 156 + }); 157 + vi.stubGlobal("fetch", mockFetch); 158 + 159 + await expect(getFinalUrl("https://example.com/test")).rejects.toThrow(); 160 + }); 161 + 162 + it("should handle non-Error exceptions", async () => { 163 + const mockFetch = vi.fn().mockRejectedValue("string error"); 164 + vi.stubGlobal("fetch", mockFetch); 165 + 166 + await expect(getFinalUrl("https://example.com/test")).rejects.toBe( 167 + "string error", 168 + ); 169 + }); 170 + }); 171 + 172 + describe("URL redirect chains", () => { 173 + it("should handle multiple redirects", async () => { 174 + const mockFetch = vi.fn().mockResolvedValue({ 175 + url: "https://example.com/final-destination", 176 + }); 177 + vi.stubGlobal("fetch", mockFetch); 178 + 179 + const result = await getFinalUrl("https://t.co/shortlink"); 180 + 181 + expect(result).toBe("https://example.com/final-destination"); 182 + }); 183 + 184 + it("should preserve query parameters in final URL", async () => { 185 + const mockFetch = vi.fn().mockResolvedValue({ 186 + url: "https://example.com/page?param=value&other=test", 187 + }); 188 + vi.stubGlobal("fetch", mockFetch); 189 + 190 + const result = await getFinalUrl("https://example.com/redirect"); 191 + 192 + expect(result).toBe("https://example.com/page?param=value&other=test"); 193 + }); 194 + 195 + it("should handle fragment identifiers", async () => { 196 + const mockFetch = vi.fn().mockResolvedValue({ 197 + url: "https://example.com/page#section", 198 + }); 199 + vi.stubGlobal("fetch", mockFetch); 200 + 201 + const result = await getFinalUrl("https://example.com/redirect"); 202 + 203 + expect(result).toBe("https://example.com/page#section"); 204 + }); 205 + }); 206 + 207 + describe("edge cases", () => { 208 + it("should handle URLs with special characters", async () => { 209 + const mockFetch = vi.fn().mockResolvedValue({ 210 + url: "https://example.com/page?query=hello%20world", 211 + }); 212 + vi.stubGlobal("fetch", mockFetch); 213 + 214 + const result = await getFinalUrl( 215 + "https://example.com/page?query=hello%20world", 216 + ); 217 + 218 + expect(result).toBe("https://example.com/page?query=hello%20world"); 219 + }); 220 + 221 + it("should handle internationalized domain names", async () => { 222 + const mockFetch = vi.fn().mockResolvedValue({ 223 + url: "https://xn--example-r63b.com/", 224 + }); 225 + vi.stubGlobal("fetch", mockFetch); 226 + 227 + const result = await getFinalUrl("https://example.com/redirect"); 228 + 229 + expect(result).toBe("https://xn--example-r63b.com/"); 230 + }); 231 + }); 232 + 233 + describe("error serialization", () => { 234 + it("should properly serialize Error objects", async () => { 235 + const error = new Error("Test error"); 236 + error.cause = "underlying cause"; 237 + 238 + const mockFetch = vi.fn().mockRejectedValue(error); 239 + vi.stubGlobal("fetch", mockFetch); 240 + 241 + await expect(getFinalUrl("https://example.com/test")).rejects.toThrow( 242 + "Test error", 243 + ); 244 + }); 245 + 246 + it("should handle errors without cause", async () => { 247 + const error = new Error("Simple error"); 248 + const mockFetch = vi.fn().mockRejectedValue(error); 249 + vi.stubGlobal("fetch", mockFetch); 250 + 251 + await expect(getFinalUrl("https://example.com/test")).rejects.toThrow( 252 + "Simple error", 253 + ); 254 + }); 255 + }); 256 + });
+157
src/utils/getLanguage.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { getLanguage } from "./getLanguage.js"; 3 + 4 + // Mock the logger 5 + vi.mock("../logger.js", () => ({ 6 + logger: { 7 + warn: vi.fn(), 8 + }, 9 + })); 10 + 11 + describe("getLanguage", () => { 12 + beforeEach(() => { 13 + vi.clearAllMocks(); 14 + }); 15 + 16 + describe("language detection", () => { 17 + it("should detect English text", async () => { 18 + const text = "Hello world, this is a test of the English language."; 19 + const result = await getLanguage(text); 20 + expect(result).toBe("eng"); 21 + }); 22 + 23 + it("should detect Spanish text", async () => { 24 + const text = 25 + "Hola mundo, esta es una prueba del idioma español con suficiente texto para detectar."; 26 + const result = await getLanguage(text); 27 + expect(result).toBe("spa"); 28 + }); 29 + 30 + it("should detect French text", async () => { 31 + const text = 32 + "Bonjour le monde, ceci est un test de la langue française avec suffisamment de texte."; 33 + const result = await getLanguage(text); 34 + expect(result).toBe("fra"); 35 + }); 36 + 37 + it("should detect German text", async () => { 38 + const text = 39 + "Hallo Welt, dies ist ein Test der deutschen Sprache mit genügend Text."; 40 + const result = await getLanguage(text); 41 + expect(result).toBe("deu"); 42 + }); 43 + 44 + it("should detect Portuguese text", async () => { 45 + const text = 46 + "Olá mundo, este é um teste da língua portuguesa com texto suficiente para detecção."; 47 + const result = await getLanguage(text); 48 + expect(result).toBe("por"); 49 + }); 50 + 51 + it("should detect Italian text", async () => { 52 + const text = 53 + "Ciao mondo, questo è un test della lingua italiana con abbastanza testo."; 54 + const result = await getLanguage(text); 55 + expect(result).toBe("ita"); 56 + }); 57 + 58 + it("should detect Japanese text", async () => { 59 + const text = "これは日本語のテストです。十分なテキストで言語を検出します。"; 60 + const result = await getLanguage(text); 61 + expect(result).toBe("jpn"); 62 + }); 63 + }); 64 + 65 + describe("edge cases", () => { 66 + it("should default to eng for empty strings", async () => { 67 + const result = await getLanguage(""); 68 + expect(result).toBe("eng"); 69 + }); 70 + 71 + it("should default to eng for whitespace-only strings", async () => { 72 + const result = await getLanguage(" "); 73 + expect(result).toBe("eng"); 74 + }); 75 + 76 + it("should default to eng for very short text", async () => { 77 + const result = await getLanguage("hi"); 78 + expect(result).toBe("eng"); 79 + }); 80 + 81 + it("should default to eng for undetermined language", async () => { 82 + const result = await getLanguage("123 456 789"); 83 + expect(result).toBe("eng"); 84 + }); 85 + 86 + it("should default to eng for symbols only", async () => { 87 + const result = await getLanguage("!@#$%^&*()"); 88 + expect(result).toBe("eng"); 89 + }); 90 + }); 91 + 92 + describe("invalid input handling", () => { 93 + it("should handle non-string input gracefully", async () => { 94 + const result = await getLanguage(123 as any); 95 + expect(result).toBe("eng"); 96 + }); 97 + 98 + it("should handle null input gracefully", async () => { 99 + const result = await getLanguage(null as any); 100 + expect(result).toBe("eng"); 101 + }); 102 + 103 + it("should handle undefined input gracefully", async () => { 104 + const result = await getLanguage(undefined as any); 105 + expect(result).toBe("eng"); 106 + }); 107 + 108 + it("should handle object input gracefully", async () => { 109 + const result = await getLanguage({} as any); 110 + expect(result).toBe("eng"); 111 + }); 112 + 113 + it("should handle array input gracefully", async () => { 114 + const result = await getLanguage([] as any); 115 + expect(result).toBe("eng"); 116 + }); 117 + }); 118 + 119 + describe("trimming behavior", () => { 120 + it("should trim leading whitespace", async () => { 121 + const text = 122 + " Hello world, this is a test of the English language."; 123 + const result = await getLanguage(text); 124 + expect(result).toBe("eng"); 125 + }); 126 + 127 + it("should trim trailing whitespace", async () => { 128 + const text = 129 + "Hello world, this is a test of the English language. "; 130 + const result = await getLanguage(text); 131 + expect(result).toBe("eng"); 132 + }); 133 + 134 + it("should trim both leading and trailing whitespace", async () => { 135 + const text = 136 + " Hello world, this is a test of the English language. "; 137 + const result = await getLanguage(text); 138 + expect(result).toBe("eng"); 139 + }); 140 + }); 141 + 142 + describe("mixed language text", () => { 143 + it("should detect primary language in mixed content", async () => { 144 + const text = 145 + "This is primarily English text with some español words mixed in."; 146 + const result = await getLanguage(text); 147 + expect(result).toBe("eng"); 148 + }); 149 + 150 + it("should handle code mixed with text", async () => { 151 + const text = 152 + "Here is some English text with const x = 123; code in it."; 153 + const result = await getLanguage(text); 154 + expect(result).toBe("eng"); 155 + }); 156 + }); 157 + });
+5
src/utils/homoglyphs.ts
··· 42 42 a: "a", 43 43 "@": "a", 44 44 "4": "a", 45 + а: "a", // cyrillic a 45 46 46 47 // Confusables for 'e' 47 48 "3": "e", ··· 88 89 ꬴ: "e", 89 90 ꬳ: "e", 90 91 e: "e", 92 + ё: "e", // cyrillic io 91 93 92 94 // Confusables for 'g' 93 95 ǵ: "g", ··· 139 141 ı: "i", 140 142 i: "i", 141 143 "1": "i", 144 + і: "i", // cyrillic i 142 145 ĺ: "l", 143 146 ľ: "l", 144 147 ļ: "l", ··· 252 255 ⱺ: "o", 253 256 o: "o", 254 257 "0": "o", 258 + о: "o", // cyrillic o 255 259 256 260 // Confusables for 'r' 257 261 ŕ: "r", ··· 305 309 ᵵ: "t", 306 310 ƫ: "t", 307 311 ȶ: "t", 312 + т: "t", // cyrillic t 308 313 };
+99
src/utils/normalizeUnicode.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { normalizeUnicode } from "./normalizeUnicode.js"; 3 + 4 + describe("normalizeUnicode", () => { 5 + describe("lowercase conversion", () => { 6 + it("should convert uppercase to lowercase", () => { 7 + expect(normalizeUnicode("HELLO")).toBe("hello"); 8 + expect(normalizeUnicode("WoRlD")).toBe("world"); 9 + }); 10 + }); 11 + 12 + describe("homoglyph replacement", () => { 13 + it("should replace homoglyphs with ASCII equivalents", () => { 14 + expect(normalizeUnicode("h3ll0")).toBe("hello"); 15 + expect(normalizeUnicode("t3st")).toBe("test"); 16 + expect(normalizeUnicode("@ppl3")).toBe("apple"); 17 + }); 18 + 19 + it("should replace accented characters", () => { 20 + expect(normalizeUnicode("café")).toBe("cafe"); 21 + expect(normalizeUnicode("naïve")).toBe("naive"); 22 + expect(normalizeUnicode("résumé")).toBe("resume"); 23 + }); 24 + 25 + it("should handle cyrillic lookalikes", () => { 26 + expect(normalizeUnicode("tеst")).toBe("test"); // е is cyrillic 27 + expect(normalizeUnicode("пight")).toBe("night"); // п is cyrillic 28 + }); 29 + }); 30 + 31 + describe("diacritic removal", () => { 32 + it("should remove combining diacritical marks", () => { 33 + expect(normalizeUnicode("e\u0301")).toBe("e"); // e with combining acute accent 34 + expect(normalizeUnicode("a\u0300")).toBe("a"); // a with combining grave accent 35 + }); 36 + 37 + it("should handle precomposed characters", () => { 38 + expect(normalizeUnicode("é")).toBe("e"); 39 + expect(normalizeUnicode("ñ")).toBe("n"); 40 + expect(normalizeUnicode("ü")).toBe("u"); 41 + }); 42 + }); 43 + 44 + describe("unicode normalization", () => { 45 + it("should normalize compatibility characters", () => { 46 + expect(normalizeUnicode("fi")).toBe("fi"); // ligature fi 47 + expect(normalizeUnicode("a")).toBe("a"); // fullwidth a 48 + }); 49 + 50 + it("should handle complex unicode sequences", () => { 51 + const input = "Ḧëḷḷö Ẅöṛḷḋ"; 52 + const expected = "hello world"; 53 + expect(normalizeUnicode(input)).toBe(expected); 54 + }); 55 + }); 56 + 57 + describe("edge cases", () => { 58 + it("should handle empty strings", () => { 59 + expect(normalizeUnicode("")).toBe(""); 60 + }); 61 + 62 + it("should handle strings with only spaces", () => { 63 + expect(normalizeUnicode(" ")).toBe(" "); 64 + }); 65 + 66 + it("should preserve non-mapped characters", () => { 67 + expect(normalizeUnicode("hello!@#$%")).toBe("hello!a#$%"); // @ maps to a 68 + }); 69 + 70 + it("should handle mixed scripts", () => { 71 + const input = "hëllö wörld"; 72 + expect(normalizeUnicode(input)).toBe("hello world"); 73 + }); 74 + 75 + it("should be idempotent", () => { 76 + const input = "tést"; 77 + const normalized = normalizeUnicode(input); 78 + expect(normalizeUnicode(normalized)).toBe(normalized); 79 + }); 80 + }); 81 + 82 + describe("real-world examples", () => { 83 + it("should normalize common slur evasion techniques", () => { 84 + expect(normalizeUnicode("f@gg0t")).toBe("faggot"); 85 + expect(normalizeUnicode("n1gg3r")).toBe("nigger"); 86 + expect(normalizeUnicode("k1k3")).toBe("kike"); 87 + }); 88 + 89 + it("should normalize unicode evasion techniques", () => { 90 + expect(normalizeUnicode("fаggоt")).toBe("faggot"); // cyrillic а and о 91 + expect(normalizeUnicode("nіggеr")).toBe("nigger"); // cyrillic і and е 92 + }); 93 + 94 + it("should handle multiple evasion techniques combined", () => { 95 + expect(normalizeUnicode("F@GG0Т")).toBe("faggot"); // mixed case, numbers, cyrillic 96 + expect(normalizeUnicode("n1ggёr")).toBe("nigger"); // numbers and cyrillic 97 + }); 98 + }); 99 + });
+13
vitest.config.ts
··· 7 7 coverage: { 8 8 provider: "v8", 9 9 reporter: ["text", "json", "html"], 10 + exclude: [ 11 + "node_modules/**", 12 + "dist/**", 13 + "**/*.config.*", 14 + "**/main.ts", 15 + "**/*.test.ts", 16 + ], 17 + thresholds: { 18 + lines: 60, 19 + functions: 60, 20 + branches: 60, 21 + statements: 60, 22 + }, 10 23 }, 11 24 }, 12 25 });