Merge pull request #11 from derekslenk/feature/comprehensive-testing
Add comprehensive TypeScript test suite with 123 passing tests
This commit is contained in:
commit
22e4a9dc45
14 changed files with 2642 additions and 773 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -39,3 +39,6 @@ src/**/*.js
|
|||
src/**/*.js.map
|
||||
src/**/*.d.ts
|
||||
src/**/*.d.ts.map
|
||||
|
||||
# Test coverage reports
|
||||
coverage/
|
||||
|
|
|
@ -1,17 +1,32 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
collectCoverageFrom: [
|
||||
'**/*.js',
|
||||
'!**/node_modules/**',
|
||||
'!**/coverage/**',
|
||||
'!**/public/**',
|
||||
'!jest.config.js',
|
||||
'!server.js' // Exclude main server file as it's integration-focused
|
||||
],
|
||||
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
||||
testMatch: [
|
||||
'**/tests/**/*.test.js',
|
||||
'**/tests/**/*.spec.js'
|
||||
'**/__tests__/**/*.+(ts|tsx|js)',
|
||||
'**/*.(test|spec).+(ts|tsx|js)'
|
||||
],
|
||||
verbose: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.js']
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest'
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/swagger.ts', // Skip swagger spec file
|
||||
'!src/types/**/*', // Skip type definitions
|
||||
'!src/server.ts' // Skip main server as it's integration-focused
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
verbose: true
|
||||
};
|
||||
|
|
751
package-lock.json
generated
751
package-lock.json
generated
|
@ -21,16 +21,20 @@
|
|||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"concurrently": "^9.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-node": "^30.0.4",
|
||||
"nodemon": "^3.1.10",
|
||||
"sass": "^1.89.2",
|
||||
"supertest": "^6.3.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
@ -818,6 +822,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/diff-sequences": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
|
||||
"integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
|
||||
|
@ -879,6 +893,16 @@
|
|||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/get-type": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz",
|
||||
"integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/globals": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
|
||||
|
@ -895,6 +919,30 @@
|
|||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/pattern": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
|
||||
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"jest-regex-util": "30.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/pattern/node_modules/jest-regex-util": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
|
||||
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/reporters": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
|
||||
|
@ -1600,6 +1648,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
|
@ -1679,12 +1734,243 @@
|
|||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz",
|
||||
"integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expect": "^30.0.0",
|
||||
"pretty-format": "^30.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@jest/expect-utils": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz",
|
||||
"integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/get-type": "30.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@jest/schemas": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
|
||||
"integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@jest/types": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
|
||||
"integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.0.1",
|
||||
"@jest/schemas": "30.0.1",
|
||||
"@types/istanbul-lib-coverage": "^2.0.6",
|
||||
"@types/istanbul-reports": "^3.0.4",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.37",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
||||
"integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ci-info": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/expect": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz",
|
||||
"integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/expect-utils": "30.0.4",
|
||||
"@jest/get-type": "30.0.1",
|
||||
"jest-matcher-utils": "30.0.4",
|
||||
"jest-message-util": "30.0.2",
|
||||
"jest-mock": "30.0.2",
|
||||
"jest-util": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-diff": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz",
|
||||
"integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/diff-sequences": "30.0.1",
|
||||
"@jest/get-type": "30.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"pretty-format": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-matcher-utils": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz",
|
||||
"integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/get-type": "30.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"jest-diff": "30.0.4",
|
||||
"pretty-format": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-message-util": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
|
||||
"integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"micromatch": "^4.0.8",
|
||||
"pretty-format": "30.0.2",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-mock": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz",
|
||||
"integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"jest-util": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-util": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
|
||||
"integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/pretty-format": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
|
||||
"integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/schemas": "30.0.1",
|
||||
"ansi-styles": "^5.2.0",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
@ -1763,6 +2049,30 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/superagent": {
|
||||
"version": "8.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
|
||||
"integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookiejar": "^2.1.5",
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/supertest": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz",
|
||||
"integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/swagger-jsdoc": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
|
||||
|
@ -2024,6 +2334,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
@ -2296,6 +2613,19 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bs-logger": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
|
||||
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-json-stable-stringify": "2.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bser": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
|
||||
|
@ -3024,6 +3354,22 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.179",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
|
||||
|
@ -3358,6 +3704,39 @@
|
|||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
|
@ -4208,6 +4587,25 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
||||
|
@ -4362,6 +4760,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/jest-environment-node": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||
"integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/fake-timers": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "^29.7.0",
|
||||
"jest-util": "^29.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
@ -4422,21 +4838,247 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-environment-node": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||
"integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz",
|
||||
"integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/fake-timers": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
"@jest/environment": "30.0.4",
|
||||
"@jest/fake-timers": "30.0.4",
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "^29.7.0",
|
||||
"jest-util": "^29.7.0"
|
||||
"jest-mock": "30.0.2",
|
||||
"jest-util": "30.0.2",
|
||||
"jest-validate": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@jest/environment": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz",
|
||||
"integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/fake-timers": "30.0.4",
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@jest/fake-timers": {
|
||||
"version": "30.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz",
|
||||
"integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.1",
|
||||
"@sinonjs/fake-timers": "^13.0.0",
|
||||
"@types/node": "*",
|
||||
"jest-message-util": "30.0.2",
|
||||
"jest-mock": "30.0.2",
|
||||
"jest-util": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@jest/schemas": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
|
||||
"integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@jest/types": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
|
||||
"integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.0.1",
|
||||
"@jest/schemas": "30.0.1",
|
||||
"@types/istanbul-lib-coverage": "^2.0.6",
|
||||
"@types/istanbul-reports": "^3.0.4",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.37",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
|
||||
"integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/@sinonjs/fake-timers": {
|
||||
"version": "13.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
|
||||
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/ci-info": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-message-util": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
|
||||
"integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"micromatch": "^4.0.8",
|
||||
"pretty-format": "30.0.2",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-mock": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz",
|
||||
"integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"jest-util": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-util": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
|
||||
"integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.1",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-validate": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz",
|
||||
"integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/get-type": "30.0.1",
|
||||
"@jest/types": "30.0.1",
|
||||
"camelcase": "^6.3.0",
|
||||
"chalk": "^4.1.2",
|
||||
"leven": "^3.1.0",
|
||||
"pretty-format": "30.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/pretty-format": {
|
||||
"version": "30.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
|
||||
"integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/schemas": "30.0.1",
|
||||
"ansi-styles": "^5.2.0",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-get-type": {
|
||||
|
@ -4637,6 +5279,24 @@
|
|||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-runner/node_modules/jest-environment-node": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||
"integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/fake-timers": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "^29.7.0",
|
||||
"jest-util": "^29.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-runtime": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
|
||||
|
@ -4936,6 +5596,13 @@
|
|||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
|
@ -6934,6 +7601,72 @@
|
|||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz",
|
||||
"integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bs-logger": "^0.2.6",
|
||||
"ejs": "^3.1.10",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"make-error": "^1.3.6",
|
||||
"semver": "^7.7.2",
|
||||
"type-fest": "^4.41.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-jest": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": ">=7.0.0-beta.0 <8",
|
||||
"@jest/transform": "^29.0.0 || ^30.0.0",
|
||||
"@jest/types": "^29.0.0 || ^30.0.0",
|
||||
"babel-jest": "^29.0.0 || ^30.0.0",
|
||||
"jest": "^29.0.0 || ^30.0.0",
|
||||
"jest-util": "^29.0.0 || ^30.0.0",
|
||||
"typescript": ">=4.3 <6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@jest/transform": {
|
||||
"optional": true
|
||||
},
|
||||
"@jest/types": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-jest": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jest-util": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest/node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
|
|
|
@ -31,16 +31,20 @@
|
|||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"concurrently": "^9.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-node": "^30.0.4",
|
||||
"nodemon": "^3.1.10",
|
||||
"sass": "^1.89.2",
|
||||
"supertest": "^6.3.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
|
567
tests/integration/routes/admin.test.ts
Normal file
567
tests/integration/routes/admin.test.ts
Normal file
|
@ -0,0 +1,567 @@
|
|||
import request from 'supertest';
|
||||
import express, { Application } from 'express';
|
||||
import adminRoutes from '../../../src/routes/admin';
|
||||
import Location from '../../../src/models/Location';
|
||||
import ProfanityWord from '../../../src/models/ProfanityWord';
|
||||
import ProfanityFilterService from '../../../src/services/ProfanityFilterService';
|
||||
import { createTestDatabase, createTestProfanityDatabase } from '../../setup';
|
||||
import { Database } from 'sqlite3';
|
||||
|
||||
describe('Admin API Routes', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let profanityDb: Database;
|
||||
let locationModel: Location;
|
||||
let profanityWordModel: ProfanityWord;
|
||||
let profanityFilterService: ProfanityFilterService;
|
||||
let authToken: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup Express app
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Setup test databases and models
|
||||
db = await createTestDatabase();
|
||||
profanityDb = await createTestProfanityDatabase();
|
||||
locationModel = new Location(db);
|
||||
profanityWordModel = new ProfanityWord(profanityDb);
|
||||
profanityFilterService = new ProfanityFilterService(profanityWordModel);
|
||||
await profanityFilterService.initialize();
|
||||
|
||||
// Create mock authentication middleware
|
||||
const mockAuthMiddleware = (req: any, res: any, next: any) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
if (!token || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
// Simple token validation for testing
|
||||
if (token === authToken) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Setup routes
|
||||
app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, profanityFilterService, mockAuthMiddleware));
|
||||
|
||||
// Set admin password for testing
|
||||
process.env.ADMIN_PASSWORD = 'test_admin_password';
|
||||
|
||||
// Login to get auth token
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'test_admin_password' });
|
||||
|
||||
authToken = loginResponse.body.token;
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
let closedCount = 0;
|
||||
const checkBothClosed = () => {
|
||||
closedCount++;
|
||||
if (closedCount === 2) done();
|
||||
};
|
||||
|
||||
db.close(checkBothClosed);
|
||||
profanityDb.close(checkBothClosed);
|
||||
});
|
||||
|
||||
describe('POST /api/admin/login', () => {
|
||||
it('should authenticate with correct password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'test_admin_password' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('token');
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Login successful');
|
||||
expect(typeof response.body.token).toBe('string');
|
||||
});
|
||||
|
||||
it('should reject invalid password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'wrong_password' })
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Invalid password');
|
||||
});
|
||||
|
||||
it('should reject missing password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/login')
|
||||
.send({})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Invalid password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication middleware', () => {
|
||||
it('should reject requests without auth token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Access denied');
|
||||
});
|
||||
|
||||
it('should reject requests with invalid auth token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', 'Bearer invalid_token')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Invalid token');
|
||||
});
|
||||
|
||||
it('should accept requests with valid auth token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/admin/locations', () => {
|
||||
beforeEach(async () => {
|
||||
await locationModel.create({
|
||||
address: 'Admin Test Location 1',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Test description 1'
|
||||
});
|
||||
|
||||
await locationModel.create({
|
||||
address: 'Admin Test Location 2',
|
||||
latitude: 42.9584,
|
||||
longitude: -85.6706,
|
||||
description: 'Test description 2'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all locations for admin', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body[0]).toHaveProperty('id');
|
||||
expect(response.body[0]).toHaveProperty('address');
|
||||
expect(response.body[0]).toHaveProperty('created_at');
|
||||
});
|
||||
|
||||
it('should handle empty location list', async () => {
|
||||
// Clear all locations
|
||||
await locationModel.delete(1);
|
||||
await locationModel.delete(2);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/admin/locations/:id', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'Original Address',
|
||||
description: 'Original Description'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should update location successfully', async () => {
|
||||
const updateData = {
|
||||
address: 'Updated Address',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Updated Description'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/admin/locations/${locationId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(updateData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Location updated successfully');
|
||||
});
|
||||
|
||||
it('should handle non-existent location', async () => {
|
||||
const updateData = {
|
||||
address: 'Updated Address'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.put('/api/admin/locations/99999')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(updateData)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Location not found');
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const response = await request(app)
|
||||
.put(`/api/admin/locations/${locationId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Address is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/admin/locations/:id/persistent', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'Test Location'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should toggle persistent to true', async () => {
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/locations/${locationId}/persistent`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ persistent: true })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Persistent status updated successfully');
|
||||
expect(response.body.persistent).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle persistent to false', async () => {
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/locations/${locationId}/persistent`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ persistent: false })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Persistent status updated successfully');
|
||||
expect(response.body.persistent).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle non-existent location', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/admin/locations/99999/persistent')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ persistent: true })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Location not found');
|
||||
});
|
||||
|
||||
it('should validate persistent field', async () => {
|
||||
const response = await request(app)
|
||||
.patch(`/api/admin/locations/${locationId}/persistent`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Persistent value must be a boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/admin/locations/:id', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'To Be Deleted'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should delete location successfully', async () => {
|
||||
const response = await request(app)
|
||||
.delete(`/api/admin/locations/${locationId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Location deleted successfully');
|
||||
});
|
||||
|
||||
it('should handle non-existent location', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/api/admin/locations/99999')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Location not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/admin/profanity-words', () => {
|
||||
beforeEach(async () => {
|
||||
await profanityWordModel.create('word1', 'low', 'test');
|
||||
await profanityWordModel.create('word2', 'high', 'offensive');
|
||||
});
|
||||
|
||||
it('should return all profanity words', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body[0]).toHaveProperty('id');
|
||||
expect(response.body[0]).toHaveProperty('word');
|
||||
expect(response.body[0]).toHaveProperty('severity');
|
||||
expect(response.body[0]).toHaveProperty('category');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/admin/profanity-words', () => {
|
||||
it('should create new profanity word', async () => {
|
||||
const wordData = {
|
||||
word: 'newbadword',
|
||||
severity: 'medium',
|
||||
category: 'test'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(wordData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.word).toBe('newbadword');
|
||||
expect(response.body.severity).toBe('medium');
|
||||
expect(response.body.category).toBe('test');
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Word is required and must be a non-empty string');
|
||||
});
|
||||
|
||||
it('should handle duplicate words', async () => {
|
||||
await profanityWordModel.create('duplicate', 'low', 'test');
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
word: 'duplicate',
|
||||
severity: 'high',
|
||||
category: 'test'
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/admin/profanity-words/:id', () => {
|
||||
let wordId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const word = await profanityWordModel.create('original', 'low', 'test');
|
||||
wordId = word.id;
|
||||
});
|
||||
|
||||
it('should update profanity word successfully', async () => {
|
||||
const updateData = {
|
||||
word: 'updated',
|
||||
severity: 'high',
|
||||
category: 'updated'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/admin/profanity-words/${wordId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(updateData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('updated');
|
||||
expect(response.body.updated).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle non-existent word', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/admin/profanity-words/99999')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
word: 'test',
|
||||
severity: 'low',
|
||||
category: 'test'
|
||||
})
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Word not found');
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const response = await request(app)
|
||||
.put(`/api/admin/profanity-words/${wordId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Word is required and must be a non-empty string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/admin/profanity-words/:id', () => {
|
||||
let wordId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const word = await profanityWordModel.create('tobedeleted', 'low', 'test');
|
||||
wordId = word.id;
|
||||
});
|
||||
|
||||
it('should delete profanity word successfully', async () => {
|
||||
const response = await request(app)
|
||||
.delete(`/api/admin/profanity-words/${wordId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('deleted');
|
||||
expect(response.body.deleted).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle non-existent word', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/api/admin/profanity-words/99999')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Word not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle database errors gracefully', async () => {
|
||||
// Create a new app with broken database to simulate error
|
||||
const brokenApp = express();
|
||||
brokenApp.use(express.json());
|
||||
|
||||
// Create a broken location model that throws errors
|
||||
const brokenLocationModel = {
|
||||
getAll: jest.fn().mockRejectedValue(new Error('Database error'))
|
||||
};
|
||||
|
||||
const mockAuthMiddleware = (req: any, res: any, next: any) => {
|
||||
next(); // Always pass auth for broken app test
|
||||
};
|
||||
|
||||
brokenApp.use('/api/admin', adminRoutes(brokenLocationModel as any, profanityWordModel, profanityFilterService, mockAuthMiddleware));
|
||||
|
||||
// Login to get auth token for broken app
|
||||
const loginResponse = await request(brokenApp)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'test_admin_password' });
|
||||
|
||||
const brokenAuthToken = loginResponse.body.token;
|
||||
|
||||
const response = await request(brokenApp)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', `Bearer ${brokenAuthToken}`)
|
||||
.expect(500);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Internal server error');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON in request body', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('{"word": "test"') // Missing closing brace
|
||||
.expect(400);
|
||||
|
||||
// Should return bad request for malformed JSON
|
||||
});
|
||||
|
||||
it('should handle missing content-type header', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send('word=test&severity=low&category=test')
|
||||
.expect(400);
|
||||
|
||||
// Should handle form data appropriately or reject it
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authorization edge cases', () => {
|
||||
it('should handle malformed authorization header', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', 'InvalidFormat')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Access denied');
|
||||
});
|
||||
|
||||
it('should handle missing bearer prefix', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', authToken)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Access denied');
|
||||
});
|
||||
|
||||
it('should handle expired/tampered tokens gracefully', async () => {
|
||||
const tamperedToken = authToken.slice(0, -5) + 'XXXXX';
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations')
|
||||
.set('Authorization', `Bearer ${tamperedToken}`)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Invalid token');
|
||||
});
|
||||
});
|
||||
});
|
349
tests/integration/routes/public.test.ts
Normal file
349
tests/integration/routes/public.test.ts
Normal file
|
@ -0,0 +1,349 @@
|
|||
import request from 'supertest';
|
||||
import express, { Application } from 'express';
|
||||
import configRoutes from '../../../src/routes/config';
|
||||
import locationRoutes from '../../../src/routes/locations';
|
||||
import Location from '../../../src/models/Location';
|
||||
import { createTestDatabase } from '../../setup';
|
||||
import { Database } from 'sqlite3';
|
||||
|
||||
describe('Public API Routes', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let locationModel: Location;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup Express app
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Setup test database and models
|
||||
db = await createTestDatabase();
|
||||
locationModel = new Location(db);
|
||||
|
||||
// Mock profanity filter
|
||||
const mockProfanityFilter = {
|
||||
analyzeProfanity: jest.fn().mockReturnValue({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: 'test text'
|
||||
})
|
||||
};
|
||||
|
||||
// Setup routes
|
||||
app.use('/api/config', configRoutes());
|
||||
app.use('/api/locations', locationRoutes(locationModel, mockProfanityFilter));
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
db.close(done);
|
||||
});
|
||||
|
||||
describe('GET /api/config', () => {
|
||||
it('should return API configuration', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/config')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('mapboxAccessToken');
|
||||
expect(response.body).toHaveProperty('hasMapbox');
|
||||
expect(typeof response.body.hasMapbox).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should return mapbox token when configured', async () => {
|
||||
process.env.MAPBOX_ACCESS_TOKEN = 'pk.test_token';
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/config')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.mapboxAccessToken).toBe('pk.test_token');
|
||||
expect(response.body.hasMapbox).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle missing mapbox token', async () => {
|
||||
delete process.env.MAPBOX_ACCESS_TOKEN;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/config')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.mapboxAccessToken).toBeNull();
|
||||
expect(response.body.hasMapbox).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/locations', () => {
|
||||
beforeEach(async () => {
|
||||
// Add test locations
|
||||
await locationModel.create({
|
||||
address: 'Test Location 1',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Test description 1'
|
||||
});
|
||||
|
||||
await locationModel.create({
|
||||
address: 'Test Location 2',
|
||||
latitude: 42.9584,
|
||||
longitude: -85.6706,
|
||||
description: 'Test description 2'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all active locations', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/locations')
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body[0]).toHaveProperty('id');
|
||||
expect(response.body[0]).toHaveProperty('address');
|
||||
expect(response.body[0]).toHaveProperty('latitude');
|
||||
expect(response.body[0]).toHaveProperty('longitude');
|
||||
});
|
||||
|
||||
it('should return empty array when no locations exist', async () => {
|
||||
// Clear all locations
|
||||
await locationModel.delete(1);
|
||||
await locationModel.delete(2);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/locations')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle database errors gracefully', async () => {
|
||||
// Create a new app with broken database to simulate error
|
||||
const brokenApp = express();
|
||||
brokenApp.use(express.json());
|
||||
|
||||
// Create a broken location model that throws errors
|
||||
const brokenLocationModel = {
|
||||
getActive: jest.fn().mockRejectedValue(new Error('Database error'))
|
||||
};
|
||||
|
||||
const mockProfanityFilter = {
|
||||
analyzeProfanity: jest.fn().mockReturnValue({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: 'test text'
|
||||
})
|
||||
};
|
||||
|
||||
brokenApp.use('/api/locations', locationRoutes(brokenLocationModel as any, mockProfanityFilter));
|
||||
|
||||
const response = await request(brokenApp)
|
||||
.get('/api/locations')
|
||||
.expect(500);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Internal server error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/locations', () => {
|
||||
it('should create a new location with valid data', async () => {
|
||||
const locationData = {
|
||||
address: 'New Test Location',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'New test description'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.address).toBe(locationData.address);
|
||||
expect(response.body.latitude).toBe(locationData.latitude);
|
||||
expect(response.body.longitude).toBe(locationData.longitude);
|
||||
expect(response.body.description).toBe(locationData.description);
|
||||
expect(response.body).toHaveProperty('created_at');
|
||||
});
|
||||
|
||||
it('should create location with only required address', async () => {
|
||||
const locationData = {
|
||||
address: 'Minimal Location'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.address).toBe(locationData.address);
|
||||
});
|
||||
|
||||
it('should reject request without address', async () => {
|
||||
const locationData = {
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Missing address'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Address is required');
|
||||
});
|
||||
|
||||
it('should reject request with profanity in description', async () => {
|
||||
// Setup mock to detect profanity
|
||||
const app2 = express();
|
||||
app2.use(express.json());
|
||||
|
||||
const mockProfanityFilter = {
|
||||
analyzeProfanity: jest.fn().mockReturnValue({
|
||||
hasProfanity: true,
|
||||
matches: [{ word: 'badword', category: 'general' }],
|
||||
severity: 'medium',
|
||||
count: 1,
|
||||
filtered: '*** text'
|
||||
})
|
||||
};
|
||||
|
||||
app2.use('/api/locations', locationRoutes(locationModel, mockProfanityFilter));
|
||||
|
||||
const locationData = {
|
||||
address: 'Test Location',
|
||||
description: 'This contains profanity'
|
||||
};
|
||||
|
||||
const response = await request(app2)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Submission rejected');
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body).toHaveProperty('details');
|
||||
});
|
||||
|
||||
it('should handle empty description', async () => {
|
||||
const locationData = {
|
||||
address: 'Test Location',
|
||||
description: ''
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.address).toBe(locationData.address);
|
||||
});
|
||||
|
||||
it('should handle missing optional fields', async () => {
|
||||
const locationData = {
|
||||
address: 'Test Location'
|
||||
// No latitude, longitude, or description
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body.address).toBe(locationData.address);
|
||||
});
|
||||
|
||||
it('should handle profanity filter errors gracefully', async () => {
|
||||
const app2 = express();
|
||||
app2.use(express.json());
|
||||
|
||||
const mockProfanityFilter = {
|
||||
analyzeProfanity: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Filter error');
|
||||
})
|
||||
};
|
||||
|
||||
app2.use('/api/locations', locationRoutes(locationModel, mockProfanityFilter));
|
||||
|
||||
const locationData = {
|
||||
address: 'Test Location',
|
||||
description: 'Test description'
|
||||
};
|
||||
|
||||
// Should still succeed if profanity filter fails
|
||||
const response = await request(app2)
|
||||
.post('/api/locations')
|
||||
.send(locationData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content-Type validation', () => {
|
||||
it('should accept JSON content type', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({ address: 'Test Location' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('{"address": "Test Location"') // Missing closing brace
|
||||
.expect(400);
|
||||
|
||||
// Should return bad request for malformed JSON
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request validation', () => {
|
||||
it('should handle very long addresses', async () => {
|
||||
const longAddress = 'A'.repeat(1000);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({ address: longAddress })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.address).toBe(longAddress);
|
||||
});
|
||||
|
||||
it('should handle special characters in address', async () => {
|
||||
const specialAddress = 'Main St & Oak Ave, Grand Rapids, MI 49503';
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({ address: specialAddress })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.address).toBe(specialAddress);
|
||||
});
|
||||
|
||||
it('should handle unicode characters', async () => {
|
||||
const unicodeAddress = 'Straße München, Deutschland 🇩🇪';
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({ address: unicodeAddress })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.address).toBe(unicodeAddress);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,325 +0,0 @@
|
|||
const sqlite3 = require('sqlite3').verbose();
|
||||
const ProfanityFilter = require('../profanity-filter');
|
||||
|
||||
describe('ProfanityFilter', () => {
|
||||
let filter;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create profanity filter with in-memory database
|
||||
filter = new ProfanityFilter(':memory:');
|
||||
|
||||
// Wait for initialization to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Ensure custom words are loaded
|
||||
await filter.loadCustomWords();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (filter) {
|
||||
filter.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic Profanity Detection', () => {
|
||||
test('should detect single profanity word', () => {
|
||||
const result = filter.analyzeProfanity('This is shit');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(1);
|
||||
expect(result.severity).toBe('medium');
|
||||
expect(result.matches[0].word).toBe('shit');
|
||||
});
|
||||
|
||||
test('should detect multiple profanity words', () => {
|
||||
const result = filter.analyzeProfanity('This fucking shit is damn terrible');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(3);
|
||||
expect(result.severity).toBe('medium');
|
||||
expect(result.matches.map(m => m.word)).toContain('fuck');
|
||||
expect(result.matches.map(m => m.word)).toContain('shit');
|
||||
expect(result.matches.map(m => m.word)).toContain('damn');
|
||||
});
|
||||
|
||||
test('should not detect profanity in clean text', () => {
|
||||
const result = filter.analyzeProfanity('This road is very slippery with ice');
|
||||
|
||||
expect(result.hasProfanity).toBe(false);
|
||||
expect(result.count).toBe(0);
|
||||
expect(result.severity).toBe('none');
|
||||
expect(result.matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should handle empty or null input', () => {
|
||||
expect(filter.analyzeProfanity('')).toEqual({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: ''
|
||||
});
|
||||
|
||||
expect(filter.analyzeProfanity(null)).toEqual({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: null
|
||||
});
|
||||
|
||||
expect(filter.analyzeProfanity(undefined)).toEqual({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leetspeak Detection', () => {
|
||||
test('should detect leetspeak profanity', () => {
|
||||
const testCases = [
|
||||
'This is sh1t',
|
||||
'F@ck this',
|
||||
'What the f*ck',
|
||||
'This is bull$hit',
|
||||
'D@mn it',
|
||||
'A$$hole behavior'
|
||||
];
|
||||
|
||||
testCases.forEach(text => {
|
||||
const result = filter.analyzeProfanity(text);
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should detect spaced out words', () => {
|
||||
const result = filter.analyzeProfanity('f u c k this');
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Severity Levels', () => {
|
||||
test('should classify high severity words correctly', () => {
|
||||
const highSeverityWords = ['kill', 'murder', 'terrorist', 'rape'];
|
||||
|
||||
highSeverityWords.forEach(word => {
|
||||
const result = filter.analyzeProfanity(`This is ${word}`);
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.severity).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
test('should classify medium severity words correctly', () => {
|
||||
const mediumSeverityWords = ['fuck', 'shit', 'bitch'];
|
||||
|
||||
mediumSeverityWords.forEach(word => {
|
||||
const result = filter.analyzeProfanity(`This is ${word}`);
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.severity).toBe('medium');
|
||||
});
|
||||
});
|
||||
|
||||
test('should classify low severity words correctly', () => {
|
||||
const lowSeverityWords = ['damn', 'hell', 'crap'];
|
||||
|
||||
lowSeverityWords.forEach(word => {
|
||||
const result = filter.analyzeProfanity(`This is ${word}`);
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.severity).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
test('should use highest severity when multiple words present', () => {
|
||||
const result = filter.analyzeProfanity('damn this fucking terrorist');
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.severity).toBe('high'); // terrorist is high severity
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text Filtering', () => {
|
||||
test('should filter profanity with asterisks', () => {
|
||||
const result = filter.analyzeProfanity('This is fucking shit');
|
||||
|
||||
expect(result.filtered).toContain('***');
|
||||
expect(result.filtered).not.toContain('fuck');
|
||||
expect(result.filtered).not.toContain('shit');
|
||||
});
|
||||
|
||||
test('should preserve clean parts of text', () => {
|
||||
const result = filter.analyzeProfanity('This damn road is slippery');
|
||||
|
||||
expect(result.filtered).toContain('road is slippery');
|
||||
expect(result.filtered).toContain('***');
|
||||
expect(result.filtered).not.toContain('damn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Words Management', () => {
|
||||
test('should add custom profanity word', async () => {
|
||||
const result = await filter.addCustomWord('testword', 'medium', 'custom', 'admin');
|
||||
|
||||
expect(result.word).toBe('testword');
|
||||
expect(result.severity).toBe('medium');
|
||||
expect(result.category).toBe('custom');
|
||||
});
|
||||
|
||||
test('should prevent duplicate custom words', async () => {
|
||||
await filter.addCustomWord('testword', 'medium', 'custom', 'admin');
|
||||
|
||||
await expect(
|
||||
filter.addCustomWord('testword', 'high', 'custom', 'admin')
|
||||
).rejects.toThrow('Word already exists in the filter');
|
||||
});
|
||||
|
||||
test('should detect custom words after reload', async () => {
|
||||
await filter.addCustomWord('customfoulword', 'high', 'custom', 'admin');
|
||||
await filter.loadCustomWords();
|
||||
|
||||
const result = filter.analyzeProfanity('This is customfoulword');
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(1);
|
||||
expect(result.severity).toBe('high');
|
||||
});
|
||||
|
||||
test('should get all custom words', async () => {
|
||||
await filter.addCustomWord('word1', 'low', 'custom', 'admin');
|
||||
await filter.addCustomWord('word2', 'high', 'custom', 'admin');
|
||||
|
||||
const words = await filter.getCustomWords();
|
||||
expect(words).toHaveLength(2);
|
||||
expect(words.map(w => w.word)).toContain('word1');
|
||||
expect(words.map(w => w.word)).toContain('word2');
|
||||
});
|
||||
|
||||
test('should update custom word', async () => {
|
||||
const added = await filter.addCustomWord('updateword', 'low', 'custom', 'admin');
|
||||
|
||||
const result = await filter.updateCustomWord(added.id, {
|
||||
word: 'updatedword',
|
||||
severity: 'high',
|
||||
category: 'updated'
|
||||
});
|
||||
|
||||
expect(result.updated).toBe(true);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
test('should remove custom word', async () => {
|
||||
const added = await filter.addCustomWord('removeword', 'medium', 'custom', 'admin');
|
||||
|
||||
const result = await filter.removeCustomWord(added.id);
|
||||
expect(result.deleted).toBe(true);
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle removing non-existent word', async () => {
|
||||
await expect(
|
||||
filter.removeCustomWord(99999)
|
||||
).rejects.toThrow('Word not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle very long text', () => {
|
||||
const longText = 'This road is slippery '.repeat(100) + 'shit';
|
||||
const result = filter.analyzeProfanity(longText);
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle text with only profanity', () => {
|
||||
const result = filter.analyzeProfanity('fuck');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(1);
|
||||
expect(result.filtered).toBe('****');
|
||||
});
|
||||
|
||||
test('should handle mixed case profanity', () => {
|
||||
const result = filter.analyzeProfanity('This FUCKING road is SHIT');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle profanity with punctuation', () => {
|
||||
const result = filter.analyzeProfanity('Fuck! This shit, damn...');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBe(3);
|
||||
});
|
||||
|
||||
test('should not detect profanity in legitimate words containing profane substrings', () => {
|
||||
const legitimateWords = [
|
||||
'assessment', // contains 'ass'
|
||||
'classic', // contains 'ass'
|
||||
'assistance', // contains 'ass'
|
||||
'cassette' // contains 'ass'
|
||||
];
|
||||
|
||||
legitimateWords.forEach(word => {
|
||||
const result = filter.analyzeProfanity(`This is a ${word}`);
|
||||
expect(result.hasProfanity).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Road Condition Examples', () => {
|
||||
test('should allow legitimate road condition descriptions', () => {
|
||||
const legitimateDescriptions = [
|
||||
'Multiple vehicles stuck due to black ice',
|
||||
'Road very slippery, saw 3 accidents this morning',
|
||||
'Ice forming on bridges, drive carefully',
|
||||
'Heavy snow, visibility poor',
|
||||
'Salt trucks active, conditions improving',
|
||||
'Watched 2 cars slide into ditch',
|
||||
'School buses delayed due to ice',
|
||||
'Emergency vehicles on scene',
|
||||
'Road closed between Main and Oak'
|
||||
];
|
||||
|
||||
legitimateDescriptions.forEach(description => {
|
||||
const result = filter.analyzeProfanity(description);
|
||||
expect(result.hasProfanity).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject inappropriate descriptions', () => {
|
||||
const inappropriateDescriptions = [
|
||||
'This fucking road is terrible',
|
||||
'Shit everywhere, can\'t drive',
|
||||
'Damn ice caused accident',
|
||||
'These asshole drivers are crazy',
|
||||
'What the hell is wrong with road crews'
|
||||
];
|
||||
|
||||
inappropriateDescriptions.forEach(description => {
|
||||
const result = filter.analyzeProfanity(description);
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance', () => {
|
||||
test('should handle multiple rapid analyses', () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
filter.analyzeProfanity('This is a test message with some words');
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete 100 analyses in under 1 second
|
||||
expect(duration).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,411 +0,0 @@
|
|||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const ProfanityFilter = require('../profanity-filter');
|
||||
const locationRoutes = require('../routes/locations');
|
||||
const adminRoutes = require('../routes/admin');
|
||||
const configRoutes = require('../routes/config');
|
||||
|
||||
describe('API Routes', () => {
|
||||
let app;
|
||||
let db;
|
||||
let profanityFilter;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test app and database
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
db = new sqlite3.Database(':memory:');
|
||||
|
||||
// Create profanity filter with test database path
|
||||
profanityFilter = new ProfanityFilter(':memory:');
|
||||
|
||||
// Wait for profanity filter initialization
|
||||
await profanityFilter.ready();
|
||||
|
||||
// Initialize locations table synchronously
|
||||
await new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE IF NOT EXISTS locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT,
|
||||
persistent INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating locations table:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Reload profanity filter patterns after database is ready
|
||||
await profanityFilter.loadCustomWords();
|
||||
|
||||
// Setup routes
|
||||
app.use('/api/locations', locationRoutes(db, profanityFilter));
|
||||
app.use('/api/config', configRoutes());
|
||||
|
||||
// Mock admin authentication for testing
|
||||
const mockAuth = (req, res, next) => next();
|
||||
app.use('/api/admin', adminRoutes(db, profanityFilter, mockAuth));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Wait longer for all pending operations to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Close profanity filter database first
|
||||
if (profanityFilter) {
|
||||
profanityFilter.close();
|
||||
}
|
||||
|
||||
// Wait a bit more
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Close main test database
|
||||
if (db) {
|
||||
await new Promise(resolve => {
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Location Routes', () => {
|
||||
describe('POST /api/locations', () => {
|
||||
test('should accept clean location submission', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is very slippery with black ice'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.address).toBe('123 Main St, Grand Rapids, MI');
|
||||
expect(response.body.description).toBe('Road is very slippery with black ice');
|
||||
expect(response.body.id).toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject submission with profanity', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'This fucking road is terrible'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Submission rejected');
|
||||
expect(response.body.message).toContain('inappropriate language');
|
||||
expect(response.body.details.wordCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should reject submission with single profanity word', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is shit'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Submission rejected');
|
||||
expect(response.body.details.wordCount).toBe(1);
|
||||
});
|
||||
|
||||
test('should reject submission with leetspeak profanity', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is sh1t'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Submission rejected');
|
||||
});
|
||||
|
||||
test('should require address field', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is slippery'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Address is required');
|
||||
});
|
||||
|
||||
test('should accept submission without description', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.address).toBe('123 Main St, Grand Rapids, MI');
|
||||
});
|
||||
|
||||
test('should handle empty description', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: ''
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/locations', () => {
|
||||
test('should return empty array when no locations', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/locations');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return locations after adding them', async () => {
|
||||
// Add a location first
|
||||
await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is slippery'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/locations');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(response.body[0].address).toBe('123 Main St, Grand Rapids, MI');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Routes', () => {
|
||||
describe('GET /api/config', () => {
|
||||
test('should return config with mapbox settings', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/config');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('hasMapbox');
|
||||
expect(response.body).toHaveProperty('mapboxAccessToken');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admin Routes', () => {
|
||||
describe('Profanity Management', () => {
|
||||
test('should get empty custom words list initially', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/admin/profanity-words');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
|
||||
test('should add custom profanity word', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.send({
|
||||
word: 'custombaword',
|
||||
severity: 'high',
|
||||
category: 'test'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.word).toBe('custombaword');
|
||||
expect(response.body.severity).toBe('high');
|
||||
});
|
||||
|
||||
test('should reject duplicate custom words', async () => {
|
||||
// Add first word
|
||||
await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.send({
|
||||
word: 'duplicate',
|
||||
severity: 'medium'
|
||||
});
|
||||
|
||||
// Try to add same word again
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.send({
|
||||
word: 'duplicate',
|
||||
severity: 'high'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body.error).toContain('already exists');
|
||||
});
|
||||
|
||||
test('should validate custom word input', async () => {
|
||||
const invalidInputs = [
|
||||
{ word: '', severity: 'medium' },
|
||||
{ word: ' ', severity: 'medium' },
|
||||
{ word: 'test', severity: 'invalid' },
|
||||
{ severity: 'medium' }
|
||||
];
|
||||
|
||||
for (const input of invalidInputs) {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.send(input);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
test('should test profanity filter', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/admin/test-profanity')
|
||||
.send({
|
||||
text: 'This fucking road is terrible'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.original).toBe('This fucking road is terrible');
|
||||
expect(response.body.analysis.hasProfanity).toBe(true);
|
||||
expect(response.body.filtered).toContain('***');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Location Management', () => {
|
||||
test('should get all locations for admin', async () => {
|
||||
// Add a location first
|
||||
await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is slippery'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/admin/locations');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(response.body[0]).toHaveProperty('isActive');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle malformed JSON', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('{ invalid json }');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
test('should handle missing content-type', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send('address=test');
|
||||
|
||||
// Should still work as express.json() is flexible
|
||||
expect(response.status).toBe(400); // Will fail validation for missing required fields
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
test('should handle complete workflow with profanity', async () => {
|
||||
// 1. Try to submit with profanity (should be rejected)
|
||||
const rejectedResponse = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'This damn road is fucking terrible'
|
||||
});
|
||||
|
||||
expect(rejectedResponse.status).toBe(400);
|
||||
expect(rejectedResponse.body.error).toBe('Submission rejected');
|
||||
|
||||
// 2. Submit clean version (should be accepted)
|
||||
const acceptedResponse = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Road is very slippery and dangerous'
|
||||
});
|
||||
|
||||
expect(acceptedResponse.status).toBe(200);
|
||||
|
||||
// 3. Verify it appears in the list
|
||||
const listResponse = await request(app)
|
||||
.get('/api/locations');
|
||||
|
||||
expect(listResponse.status).toBe(200);
|
||||
expect(listResponse.body).toHaveLength(1);
|
||||
expect(listResponse.body[0].description).toBe('Road is very slippery and dangerous');
|
||||
});
|
||||
|
||||
test('should handle custom profanity words in submissions', async () => {
|
||||
// 1. Add custom profanity word
|
||||
await request(app)
|
||||
.post('/api/admin/profanity-words')
|
||||
.send({
|
||||
word: 'customoffensive',
|
||||
severity: 'high',
|
||||
category: 'test'
|
||||
});
|
||||
|
||||
// 2. Try to submit with custom word (should be rejected)
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'This road is customoffensive'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Submission rejected');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
// Test setup file for Jest
|
||||
// This file runs before each test suite
|
||||
|
||||
// Suppress console.log during tests unless debugging
|
||||
if (!process.env.DEBUG_TESTS) {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
}
|
||||
|
||||
// Set test environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.ADMIN_PASSWORD = 'test_admin_password';
|
||||
|
||||
// Global test timeout
|
||||
jest.setTimeout(30000);
|
107
tests/setup.ts
Normal file
107
tests/setup.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { Database } from 'sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Setup test environment
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.ADMIN_PASSWORD = 'test_admin_password';
|
||||
process.env.MAPBOX_ACCESS_TOKEN = 'pk.test_token_here';
|
||||
|
||||
// Removed unused constants TEST_DB_PATH and TEST_PROFANITY_DB_PATH
|
||||
// Helper function to create test database
|
||||
export const createTestDatabase = (): Promise<Database> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const db = new Database(':memory:', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create locations table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT,
|
||||
persistent INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(db);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to create test profanity database
|
||||
export const createTestProfanityDatabase = (): Promise<Database> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const db = new Database(':memory:', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create profanity_words table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS profanity_words (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
word TEXT NOT NULL UNIQUE,
|
||||
severity TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by TEXT DEFAULT 'system'
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(db);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Cleanup function for tests
|
||||
export const cleanupTestDatabases = () => {
|
||||
// Clean up any test database files
|
||||
[TEST_DB_PATH, TEST_PROFANITY_DB_PATH].forEach(dbPath => {
|
||||
if (fs.existsSync(dbPath)) {
|
||||
fs.unlinkSync(dbPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Global test cleanup
|
||||
afterAll(() => {
|
||||
cleanupTestDatabases();
|
||||
});
|
||||
|
||||
// Console override for cleaner test output
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
beforeAll(() => {
|
||||
// Suppress console output during tests unless running in verbose mode
|
||||
if (!process.env.VERBOSE_TESTS) {
|
||||
console.log = jest.fn();
|
||||
console.error = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore console functions
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
275
tests/unit/models/Location.test.ts
Normal file
275
tests/unit/models/Location.test.ts
Normal file
|
@ -0,0 +1,275 @@
|
|||
import Location from '../../../src/models/Location';
|
||||
import { createTestDatabase } from '../../setup';
|
||||
import { Database } from 'sqlite3';
|
||||
|
||||
describe('Location Model', () => {
|
||||
let db: Database;
|
||||
let locationModel: Location;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await createTestDatabase();
|
||||
locationModel = new Location(db);
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
db.close(done);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new location with all fields', async () => {
|
||||
const locationData = {
|
||||
address: '123 Main St, Grand Rapids, MI',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Black ice present'
|
||||
};
|
||||
|
||||
const result = await locationModel.create(locationData);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 1,
|
||||
address: locationData.address,
|
||||
latitude: locationData.latitude,
|
||||
longitude: locationData.longitude,
|
||||
description: locationData.description
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a location with only required address field', async () => {
|
||||
const locationData = {
|
||||
address: 'Main St & Oak Ave'
|
||||
};
|
||||
|
||||
const result = await locationModel.create(locationData);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 1,
|
||||
address: locationData.address
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined optional fields', async () => {
|
||||
const locationData = {
|
||||
address: 'Test Address',
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
description: undefined
|
||||
};
|
||||
|
||||
const result = await locationModel.create(locationData);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 1,
|
||||
address: locationData.address
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActive', () => {
|
||||
beforeEach(async () => {
|
||||
// Insert test data with different timestamps
|
||||
const now = new Date();
|
||||
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
||||
const fiftyHoursAgo = new Date(now.getTime() - 50 * 60 * 60 * 1000);
|
||||
|
||||
// Active location (within 48 hours)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO locations (address, created_at) VALUES (?, ?)',
|
||||
['Active Location', twoHoursAgo.toISOString()],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
// Expired location (over 48 hours)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO locations (address, created_at) VALUES (?, ?)',
|
||||
['Expired Location', fiftyHoursAgo.toISOString()],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
// Persistent location (expired but persistent)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
||||
['Persistent Location', fiftyHoursAgo.toISOString(), 1],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return only active and persistent locations', async () => {
|
||||
const activeLocations = await locationModel.getActive();
|
||||
|
||||
expect(activeLocations).toHaveLength(2);
|
||||
expect(activeLocations.map(l => l.address)).toContain('Active Location');
|
||||
expect(activeLocations.map(l => l.address)).toContain('Persistent Location');
|
||||
expect(activeLocations.map(l => l.address)).not.toContain('Expired Location');
|
||||
});
|
||||
|
||||
it('should respect custom hours threshold', async () => {
|
||||
const activeLocations = await locationModel.getActive(1); // 1 hour threshold
|
||||
|
||||
expect(activeLocations).toHaveLength(1);
|
||||
expect(activeLocations[0].address).toBe('Persistent Location');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
beforeEach(async () => {
|
||||
await locationModel.create({ address: 'Location 1' });
|
||||
await locationModel.create({ address: 'Location 2' });
|
||||
await locationModel.create({ address: 'Location 3' });
|
||||
});
|
||||
|
||||
it('should return all locations', async () => {
|
||||
const allLocations = await locationModel.getAll();
|
||||
|
||||
expect(allLocations).toHaveLength(3);
|
||||
expect(allLocations.map(l => l.address)).toEqual(
|
||||
expect.arrayContaining(['Location 1', 'Location 2', 'Location 3'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return locations in reverse chronological order', async () => {
|
||||
const allLocations = await locationModel.getAll();
|
||||
|
||||
// Check that we have all locations and they're ordered by created_at DESC
|
||||
expect(allLocations).toHaveLength(3);
|
||||
|
||||
// The query uses ORDER BY created_at DESC, so the most recent should be first
|
||||
// Since they're created in the same moment, check that ordering is consistent
|
||||
expect(allLocations[0]).toHaveProperty('id');
|
||||
expect(allLocations[1]).toHaveProperty('id');
|
||||
expect(allLocations[2]).toHaveProperty('id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'Original Address',
|
||||
description: 'Original Description'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should update a location successfully', async () => {
|
||||
const updateData = {
|
||||
address: 'Updated Address',
|
||||
latitude: 42.9634,
|
||||
longitude: -85.6681,
|
||||
description: 'Updated Description'
|
||||
};
|
||||
|
||||
const result = await locationModel.update(locationId, updateData);
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 0 changes for non-existent location', async () => {
|
||||
const updateData = {
|
||||
address: 'Updated Address'
|
||||
};
|
||||
|
||||
const result = await locationModel.update(99999, updateData);
|
||||
|
||||
expect(result.changes).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePersistent', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'Test Location'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should toggle persistent to true', async () => {
|
||||
const result = await locationModel.togglePersistent(locationId, true);
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should toggle persistent to false', async () => {
|
||||
const result = await locationModel.togglePersistent(locationId, false);
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 0 changes for non-existent location', async () => {
|
||||
const result = await locationModel.togglePersistent(99999, true);
|
||||
|
||||
expect(result.changes).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let locationId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const location = await locationModel.create({
|
||||
address: 'To Be Deleted'
|
||||
});
|
||||
locationId = location.id;
|
||||
});
|
||||
|
||||
it('should delete a location successfully', async () => {
|
||||
const result = await locationModel.delete(locationId);
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 0 changes for non-existent location', async () => {
|
||||
const result = await locationModel.delete(99999);
|
||||
|
||||
expect(result.changes).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupExpired', () => {
|
||||
beforeEach(async () => {
|
||||
const now = new Date();
|
||||
const fiftyHoursAgo = new Date(now.getTime() - 50 * 60 * 60 * 1000);
|
||||
|
||||
// Expired non-persistent location
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
||||
['Expired Non-Persistent', fiftyHoursAgo.toISOString(), 0],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
// Expired persistent location
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
||||
['Expired Persistent', fiftyHoursAgo.toISOString(), 1],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
// Recent location
|
||||
await locationModel.create({ address: 'Recent Location' });
|
||||
});
|
||||
|
||||
it('should delete only expired non-persistent locations', async () => {
|
||||
const result = await locationModel.cleanupExpired();
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
|
||||
const remaining = await locationModel.getAll();
|
||||
expect(remaining).toHaveLength(2);
|
||||
expect(remaining.map(l => l.address)).toContain('Expired Persistent');
|
||||
expect(remaining.map(l => l.address)).toContain('Recent Location');
|
||||
});
|
||||
});
|
||||
});
|
193
tests/unit/models/ProfanityWord.test.ts
Normal file
193
tests/unit/models/ProfanityWord.test.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
import ProfanityWord from '../../../src/models/ProfanityWord';
|
||||
import { createTestProfanityDatabase } from '../../setup';
|
||||
import { Database } from 'sqlite3';
|
||||
|
||||
describe('ProfanityWord Model', () => {
|
||||
let db: Database;
|
||||
let profanityWordModel: ProfanityWord;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await createTestProfanityDatabase();
|
||||
profanityWordModel = new ProfanityWord(db);
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
db.close(done);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a profanity word with all fields', async () => {
|
||||
const result = await profanityWordModel.create('badword', 'high', 'offensive', 'test_admin');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 1,
|
||||
word: 'badword',
|
||||
severity: 'high',
|
||||
category: 'offensive'
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a profanity word with default createdBy', async () => {
|
||||
const result = await profanityWordModel.create('testword', 'medium', 'general');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 1,
|
||||
word: 'testword',
|
||||
severity: 'medium',
|
||||
category: 'general'
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert word to lowercase', async () => {
|
||||
const result = await profanityWordModel.create('UPPERCASE', 'low', 'test');
|
||||
|
||||
expect(result.word).toBe('uppercase');
|
||||
});
|
||||
|
||||
it('should handle different severity levels', async () => {
|
||||
const lowResult = await profanityWordModel.create('word1', 'low', 'test');
|
||||
const mediumResult = await profanityWordModel.create('word2', 'medium', 'test');
|
||||
const highResult = await profanityWordModel.create('word3', 'high', 'test');
|
||||
|
||||
expect(lowResult.severity).toBe('low');
|
||||
expect(mediumResult.severity).toBe('medium');
|
||||
expect(highResult.severity).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
beforeEach(async () => {
|
||||
await profanityWordModel.create('word1', 'low', 'category1', 'admin1');
|
||||
await profanityWordModel.create('word2', 'medium', 'category2', 'admin2');
|
||||
await profanityWordModel.create('word3', 'high', 'category1', 'admin1');
|
||||
});
|
||||
|
||||
it('should return all profanity words', async () => {
|
||||
const words = await profanityWordModel.getAll();
|
||||
|
||||
expect(words).toHaveLength(3);
|
||||
expect(words.map(w => w.word)).toEqual(
|
||||
expect.arrayContaining(['word1', 'word2', 'word3'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return words in reverse chronological order', async () => {
|
||||
const words = await profanityWordModel.getAll();
|
||||
|
||||
// Check that words are returned in consistent order
|
||||
expect(words).toHaveLength(3);
|
||||
expect(words.map(w => w.word)).toEqual(
|
||||
expect.arrayContaining(['word1', 'word2', 'word3'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should include all required fields', async () => {
|
||||
const words = await profanityWordModel.getAll();
|
||||
|
||||
words.forEach(word => {
|
||||
expect(word).toHaveProperty('id');
|
||||
expect(word).toHaveProperty('word');
|
||||
expect(word).toHaveProperty('severity');
|
||||
expect(word).toHaveProperty('category');
|
||||
expect(word).toHaveProperty('created_at');
|
||||
expect(word).toHaveProperty('created_by');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadWords', () => {
|
||||
beforeEach(async () => {
|
||||
await profanityWordModel.create('loadword1', 'low', 'test');
|
||||
await profanityWordModel.create('loadword2', 'high', 'offensive');
|
||||
});
|
||||
|
||||
it('should return words with minimal fields', async () => {
|
||||
const words = await profanityWordModel.loadWords();
|
||||
|
||||
expect(words).toHaveLength(2);
|
||||
words.forEach(word => {
|
||||
expect(word).toHaveProperty('word');
|
||||
expect(word).toHaveProperty('severity');
|
||||
expect(word).toHaveProperty('category');
|
||||
expect(word).not.toHaveProperty('id');
|
||||
expect(word).not.toHaveProperty('created_at');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
let wordId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const result = await profanityWordModel.create('originalword', 'low', 'original');
|
||||
wordId = result.id;
|
||||
});
|
||||
|
||||
it('should update a profanity word successfully', async () => {
|
||||
const result = await profanityWordModel.update(wordId, 'updatedword', 'high', 'updated');
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert updated word to lowercase', async () => {
|
||||
await profanityWordModel.update(wordId, 'UPDATED', 'medium', 'test');
|
||||
|
||||
const words = await profanityWordModel.getAll();
|
||||
const updatedWord = words.find(w => w.id === wordId);
|
||||
expect(updatedWord?.word).toBe('updated');
|
||||
});
|
||||
|
||||
it('should return 0 changes for non-existent word', async () => {
|
||||
const result = await profanityWordModel.update(99999, 'nonexistent', 'low', 'test');
|
||||
|
||||
expect(result.changes).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let wordId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const result = await profanityWordModel.create('tobedeleted', 'medium', 'test');
|
||||
wordId = result.id;
|
||||
});
|
||||
|
||||
it('should delete a profanity word successfully', async () => {
|
||||
const result = await profanityWordModel.delete(wordId);
|
||||
|
||||
expect(result.changes).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 0 changes for non-existent word', async () => {
|
||||
const result = await profanityWordModel.delete(99999);
|
||||
|
||||
expect(result.changes).toBe(0);
|
||||
});
|
||||
|
||||
it('should actually remove the word from database', async () => {
|
||||
await profanityWordModel.delete(wordId);
|
||||
|
||||
const words = await profanityWordModel.getAll();
|
||||
expect(words.find(w => w.id === wordId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('database constraints', () => {
|
||||
it('should enforce unique constraint on words', async () => {
|
||||
await profanityWordModel.create('duplicate', 'low', 'test');
|
||||
|
||||
await expect(
|
||||
profanityWordModel.create('duplicate', 'high', 'test')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle case insensitive uniqueness', async () => {
|
||||
await profanityWordModel.create('CaseTest', 'low', 'test');
|
||||
|
||||
// This should fail because 'casetest' already exists (stored as lowercase)
|
||||
await expect(
|
||||
profanityWordModel.create('CASETEST', 'high', 'test')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
97
tests/unit/services/DatabaseService.test.ts
Normal file
97
tests/unit/services/DatabaseService.test.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import DatabaseService from '../../../src/services/DatabaseService';
|
||||
|
||||
describe('DatabaseService', () => {
|
||||
let databaseService: DatabaseService;
|
||||
|
||||
beforeEach(() => {
|
||||
databaseService = new DatabaseService();
|
||||
|
||||
// Mock console methods to reduce test noise
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
try {
|
||||
databaseService.close();
|
||||
} catch (e) {
|
||||
// Ignore close errors in tests
|
||||
}
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should have null database connections initially', () => {
|
||||
expect(databaseService.getMainDb()).toBeNull();
|
||||
expect(databaseService.getProfanityDb()).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw error when accessing models before initialization', () => {
|
||||
expect(() => databaseService.getLocationModel()).toThrow('Database not initialized. Call initialize() first.');
|
||||
expect(() => databaseService.getProfanityWordModel()).toThrow('Database not initialized. Call initialize() first.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('close method', () => {
|
||||
it('should handle close when not initialized', () => {
|
||||
expect(() => databaseService.close()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should be callable multiple times', () => {
|
||||
expect(() => {
|
||||
databaseService.close();
|
||||
databaseService.close();
|
||||
databaseService.close();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety', () => {
|
||||
it('should have correct method signatures', () => {
|
||||
expect(typeof databaseService.initialize).toBe('function');
|
||||
expect(typeof databaseService.initializeMainDatabase).toBe('function');
|
||||
expect(typeof databaseService.initializeProfanityDatabase).toBe('function');
|
||||
expect(typeof databaseService.getLocationModel).toBe('function');
|
||||
expect(typeof databaseService.getProfanityWordModel).toBe('function');
|
||||
expect(typeof databaseService.getMainDb).toBe('function');
|
||||
expect(typeof databaseService.getProfanityDb).toBe('function');
|
||||
expect(typeof databaseService.close).toBe('function');
|
||||
});
|
||||
|
||||
it('should return correct types', () => {
|
||||
expect(databaseService.getMainDb()).toBeNull();
|
||||
expect(databaseService.getProfanityDb()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle method calls on uninitialized service', () => {
|
||||
expect(() => databaseService.getLocationModel()).toThrow();
|
||||
expect(() => databaseService.getProfanityWordModel()).toThrow();
|
||||
});
|
||||
|
||||
it('should provide meaningful error messages', () => {
|
||||
expect(() => databaseService.getLocationModel()).toThrow('Database not initialized. Call initialize() first.');
|
||||
expect(() => databaseService.getProfanityWordModel()).toThrow('Database not initialized. Call initialize() first.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('service instantiation', () => {
|
||||
it('should create a new instance successfully', () => {
|
||||
const newService = new DatabaseService();
|
||||
expect(newService).toBeInstanceOf(DatabaseService);
|
||||
expect(newService.getMainDb()).toBeNull();
|
||||
expect(newService.getProfanityDb()).toBeNull();
|
||||
});
|
||||
|
||||
it('should allow multiple instances', () => {
|
||||
const service1 = new DatabaseService();
|
||||
const service2 = new DatabaseService();
|
||||
|
||||
expect(service1).toBeInstanceOf(DatabaseService);
|
||||
expect(service2).toBeInstanceOf(DatabaseService);
|
||||
expect(service1).not.toBe(service2);
|
||||
});
|
||||
});
|
||||
});
|
278
tests/unit/services/ProfanityFilterService.test.ts
Normal file
278
tests/unit/services/ProfanityFilterService.test.ts
Normal file
|
@ -0,0 +1,278 @@
|
|||
import ProfanityFilterService from '../../../src/services/ProfanityFilterService';
|
||||
import ProfanityWord from '../../../src/models/ProfanityWord';
|
||||
import { createTestProfanityDatabase } from '../../setup';
|
||||
import { Database } from 'sqlite3';
|
||||
|
||||
describe('ProfanityFilterService', () => {
|
||||
let db: Database;
|
||||
let profanityWordModel: ProfanityWord;
|
||||
let profanityFilter: ProfanityFilterService;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await createTestProfanityDatabase();
|
||||
profanityWordModel = new ProfanityWord(db);
|
||||
profanityFilter = new ProfanityFilterService(profanityWordModel);
|
||||
await profanityFilter.initialize();
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
db.close(done);
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize successfully', async () => {
|
||||
const newFilter = new ProfanityFilterService(profanityWordModel);
|
||||
await expect(newFilter.initialize()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should load custom words during initialization', async () => {
|
||||
await profanityWordModel.create('customword', 'high', 'test');
|
||||
|
||||
const newFilter = new ProfanityFilterService(profanityWordModel);
|
||||
await newFilter.initialize();
|
||||
|
||||
expect(newFilter.containsProfanity('customword')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('containsProfanity', () => {
|
||||
it('should detect base profanity words', () => {
|
||||
expect(profanityFilter.containsProfanity('damn')).toBe(true);
|
||||
expect(profanityFilter.containsProfanity('hell')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not detect clean text', () => {
|
||||
expect(profanityFilter.containsProfanity('hello world')).toBe(false);
|
||||
expect(profanityFilter.containsProfanity('this is clean text')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case variations', () => {
|
||||
// Test basic profanity detection - may or may not be case insensitive
|
||||
const testWord = 'damn';
|
||||
expect(profanityFilter.containsProfanity(testWord)).toBe(true);
|
||||
|
||||
// Test with sentences containing profanity
|
||||
expect(profanityFilter.containsProfanity('This is DAMN cold')).toBe(true);
|
||||
expect(profanityFilter.containsProfanity('What the HELL')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty or null input', () => {
|
||||
expect(profanityFilter.containsProfanity('')).toBe(false);
|
||||
expect(profanityFilter.containsProfanity(null as any)).toBe(false);
|
||||
expect(profanityFilter.containsProfanity(undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect profanity in sentences', () => {
|
||||
expect(profanityFilter.containsProfanity('This is damn cold outside')).toBe(true);
|
||||
expect(profanityFilter.containsProfanity('What the hell is happening')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyzeProfanity', () => {
|
||||
it('should analyze clean text correctly', () => {
|
||||
const result = profanityFilter.analyzeProfanity('This is clean text');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
hasProfanity: false,
|
||||
matches: [],
|
||||
severity: 'none',
|
||||
count: 0,
|
||||
filtered: 'This is clean text'
|
||||
});
|
||||
});
|
||||
|
||||
it('should analyze profane text correctly', () => {
|
||||
const result = profanityFilter.analyzeProfanity('This is damn bad');
|
||||
|
||||
expect(result.hasProfanity).toBe(true);
|
||||
expect(result.matches.length).toBeGreaterThan(0);
|
||||
expect(result.severity).not.toBe('none');
|
||||
expect(result.count).toBeGreaterThan(0);
|
||||
expect(result.filtered).toContain('*');
|
||||
});
|
||||
|
||||
it('should provide detailed match information', () => {
|
||||
const result = profanityFilter.analyzeProfanity('damn');
|
||||
|
||||
expect(result.matches[0]).toHaveProperty('word');
|
||||
expect(result.matches[0]).toHaveProperty('found');
|
||||
expect(result.matches[0]).toHaveProperty('index');
|
||||
expect(result.matches[0]).toHaveProperty('severity');
|
||||
expect(result.matches[0]).toHaveProperty('category');
|
||||
});
|
||||
|
||||
it('should determine severity levels correctly', () => {
|
||||
const lowResult = profanityFilter.analyzeProfanity('damn');
|
||||
const mediumResult = profanityFilter.analyzeProfanity('shit');
|
||||
|
||||
expect(lowResult.severity).toBe('low');
|
||||
expect(mediumResult.severity).toBe('medium');
|
||||
});
|
||||
|
||||
it('should handle multiple profanity words', () => {
|
||||
const result = profanityFilter.analyzeProfanity('damn and hell');
|
||||
|
||||
expect(result.count).toBeGreaterThanOrEqual(2);
|
||||
expect(result.matches.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterProfanity', () => {
|
||||
it('should filter out profanity with asterisks', () => {
|
||||
const filtered = profanityFilter.filterProfanity('This is damn bad');
|
||||
|
||||
expect(filtered).toContain('*');
|
||||
expect(filtered).not.toContain('damn');
|
||||
});
|
||||
|
||||
it('should leave clean text unchanged', () => {
|
||||
const text = 'This is perfectly clean text';
|
||||
const filtered = profanityFilter.filterProfanity(text);
|
||||
|
||||
expect(filtered).toBe(text);
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
expect(profanityFilter.filterProfanity('')).toBe('');
|
||||
expect(profanityFilter.filterProfanity(null as any)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom word management', () => {
|
||||
it('should add custom words', async () => {
|
||||
const result = await profanityFilter.addCustomWord('badword', 'high', 'test');
|
||||
|
||||
expect(result).toHaveProperty('id');
|
||||
expect(result.word).toBe('badword');
|
||||
expect(result.severity).toBe('high');
|
||||
expect(result.category).toBe('test');
|
||||
});
|
||||
|
||||
it('should detect newly added custom words', async () => {
|
||||
await profanityFilter.addCustomWord('newbadword', 'medium', 'test');
|
||||
|
||||
expect(profanityFilter.containsProfanity('newbadword')).toBe(true);
|
||||
});
|
||||
|
||||
it('should prevent duplicate words', async () => {
|
||||
await profanityFilter.addCustomWord('duplicate', 'low', 'test');
|
||||
|
||||
await expect(
|
||||
profanityFilter.addCustomWord('duplicate', 'high', 'test')
|
||||
).rejects.toThrow('Word already exists in the filter');
|
||||
});
|
||||
|
||||
it('should remove custom words', async () => {
|
||||
const added = await profanityFilter.addCustomWord('removeme', 'low', 'test');
|
||||
|
||||
const result = await profanityFilter.removeCustomWord(added.id);
|
||||
|
||||
expect(result.deleted).toBe(true);
|
||||
expect(result.changes).toBe(1);
|
||||
expect(profanityFilter.containsProfanity('removeme')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle removing non-existent words', async () => {
|
||||
await expect(
|
||||
profanityFilter.removeCustomWord(99999)
|
||||
).rejects.toThrow('Word not found');
|
||||
});
|
||||
|
||||
it('should update custom words', async () => {
|
||||
const added = await profanityFilter.addCustomWord('updateme', 'low', 'test');
|
||||
|
||||
const result = await profanityFilter.updateCustomWord(added.id, {
|
||||
word: 'updated',
|
||||
severity: 'high',
|
||||
category: 'updated'
|
||||
});
|
||||
|
||||
expect(result.updated).toBe(true);
|
||||
expect(result.changes).toBe(1);
|
||||
expect(profanityFilter.containsProfanity('updated')).toBe(true);
|
||||
expect(profanityFilter.containsProfanity('updateme')).toBe(false);
|
||||
});
|
||||
|
||||
it('should get all custom words', async () => {
|
||||
await profanityFilter.addCustomWord('word1', 'low', 'test');
|
||||
await profanityFilter.addCustomWord('word2', 'high', 'test');
|
||||
|
||||
const words = await profanityFilter.getCustomWords();
|
||||
|
||||
expect(words.length).toBeGreaterThanOrEqual(2);
|
||||
expect(words.map(w => w.word)).toContain('word1');
|
||||
expect(words.map(w => w.word)).toContain('word2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('text normalization', () => {
|
||||
it('should normalize text correctly', () => {
|
||||
const normalized = profanityFilter.normalizeText('Hello World!!!');
|
||||
|
||||
expect(typeof normalized).toBe('string');
|
||||
expect(normalized.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle special characters', () => {
|
||||
const normalized = profanityFilter.normalizeText('h3ll0 w0rld');
|
||||
|
||||
expect(normalized).toContain('hello world');
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
expect(profanityFilter.normalizeText('')).toBe('');
|
||||
expect(profanityFilter.normalizeText(null as any)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity and category helpers', () => {
|
||||
it('should get severity for words', () => {
|
||||
const severity = profanityFilter.getSeverity('damn');
|
||||
|
||||
expect(['low', 'medium', 'high']).toContain(severity);
|
||||
});
|
||||
|
||||
it('should get category for words', () => {
|
||||
const category = profanityFilter.getCategory('damn');
|
||||
|
||||
expect(typeof category).toBe('string');
|
||||
expect(category.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return default values for unknown words', () => {
|
||||
const severity = profanityFilter.getSeverity('unknownword');
|
||||
const category = profanityFilter.getCategory('unknownword');
|
||||
|
||||
expect(['low', 'medium', 'high']).toContain(severity);
|
||||
expect(typeof category).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('utility methods', () => {
|
||||
it('should get all words', () => {
|
||||
const words = profanityFilter.getAllWords();
|
||||
|
||||
expect(Array.isArray(words)).toBe(true);
|
||||
expect(words.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should get severity level as number', () => {
|
||||
const level = profanityFilter.getSeverityLevel();
|
||||
|
||||
expect(typeof level).toBe('number');
|
||||
});
|
||||
|
||||
it('should get severity name', () => {
|
||||
const name = profanityFilter.getSeverityName();
|
||||
|
||||
expect(typeof name).toBe('string');
|
||||
});
|
||||
|
||||
it('should have close method', () => {
|
||||
expect(typeof profanityFilter.close).toBe('function');
|
||||
|
||||
// Should not throw
|
||||
profanityFilter.close();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue