diff --git a/package-lock.json b/package-lock.json index 88e44918..64e51513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "mocha": "^10.2.0", "npm-check-updates": "^16.14.12", "nyc": "^15.1.0", + "sinon": "^17.0.2", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", "tsconfig-paths": "^4.2.0", @@ -1458,6 +1459,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -6462,6 +6507,12 @@ "node >= 0.2.0" ] }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", @@ -6538,6 +6589,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7251,6 +7308,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node_modules/node-gyp": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", @@ -8941,6 +9011,12 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10033,6 +10109,54 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sinon": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.2.tgz", + "integrity": "sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^5.1.9", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10945,6 +11069,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -12652,6 +12785,52 @@ "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", "dev": true }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -16409,6 +16588,12 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", @@ -16467,6 +16652,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -17009,6 +17200,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node-gyp": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", @@ -18254,6 +18458,12 @@ } } }, + "path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -19034,6 +19244,43 @@ "make-fetch-happen": "^11.0.1" } }, + "sinon": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.2.tgz", + "integrity": "sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^5.1.9", + "supports-color": "^7" + }, + "dependencies": { + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -19725,6 +19972,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/package.json b/package.json index 3b541a7e..c841ab4b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "release": "npm run release:build", "test:addon": "npm run build --environment=test --target=firefox-mv2 && npx addons-linter ./dist", "test:all": "npm run test && npm run test:lint:tests", - "test:cov": "nyc --reporter=lcov --reporter=text npm run test:nocov", + "test:cov": "nyc --reporter=lcov --reporter=html --reporter=text npm run test:nocov", "test:debug": "node --inspect-brk node_modules/mocha/bin/_mocha --paths --reporter tap", "test:lint:all": "npm run test:lint && npm run test:lint:bin && npm run test:lint:tests", "test:lint:bin": "eslint \"bin/**/*.*js\"", @@ -86,6 +86,7 @@ "mocha": "^10.2.0", "npm-check-updates": "^16.14.12", "nyc": "^15.1.0", + "sinon": "^17.0.2", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", "tsconfig-paths": "^4.2.0", diff --git a/src/script/lib/filter.ts b/src/script/lib/filter.ts index c3c8f557..8a539ece 100644 --- a/src/script/lib/filter.ts +++ b/src/script/lib/filter.ts @@ -155,14 +155,8 @@ export default class Filter { sub = sub.replace(`\\${i + 1}`, this.cfg.preserveCase ? captureGroup : captureGroup.toLowerCase()); }); - // Only return if something was substituted - if (sub !== word.sub) { - if (this.cfg.substitutionMark) { - sub = '[' + sub + ']'; - } - - return sub; - } + if (this.cfg.substitutionMark) sub = '[' + sub + ']'; + return sub; } // Make substitution match case of original match diff --git a/src/script/lib/helper.ts b/src/script/lib/helper.ts index 9ecd1153..14a9016f 100644 --- a/src/script/lib/helper.ts +++ b/src/script/lib/helper.ts @@ -137,10 +137,7 @@ export function numberToBoolean(value: number): boolean { export function numberWithCommas(number: number | string): string { if (typeof Intl == 'object' && typeof Intl.NumberFormat == 'function') { - if (typeof number === 'string') { - number = parseInt(number).toString(); - } - + if (typeof number === 'string') number = parseInt(number); return number.toLocaleString(); } else { number = number.toString(); diff --git a/src/script/lib/wordlist.ts b/src/script/lib/wordlist.ts index 94fc9882..f4673b91 100644 --- a/src/script/lib/wordlist.ts +++ b/src/script/lib/wordlist.ts @@ -2,18 +2,24 @@ import Constants from './constants'; import Word from './word'; import Config from './config'; import Logger from './logger'; -const logger = new Logger('Wordlist'); export default class Wordlist { all: Word[]; list: string[]; regExps: RegExp[]; + //#region Class reference helpers + // Can be overridden in children classes + get Class() { return (this.constructor as typeof Wordlist); } + //#endregion + + static readonly logger = new Logger('Wordlist'); + constructor(cfg: Config, wordlistId: number) { this.all = []; this.list = []; this.regExps = []; - logger.setLevel(cfg.loggingLevel); + this.Class.logger.setLevel(cfg.loggingLevel); // Sort the words array by longest (most-specific) first const sorted = Object.keys(cfg.words).sort((a, b) => { @@ -29,7 +35,7 @@ export default class Wordlist { this.all.push(word); this.regExps.push(word.regExp); } catch (err) { - logger.warn(`Failed to add '${wordStr}' to wordlist.`, err); + this.Class.logger.warn(`Failed to add '${wordStr}' to wordlist.`, err); } } }); diff --git a/test/spec/lib/config.spec.ts b/test/spec/lib/config.spec.ts index 5547aef4..cfad3d3d 100644 --- a/test/spec/lib/config.spec.ts +++ b/test/spec/lib/config.spec.ts @@ -19,6 +19,13 @@ describe('Config', function() { }); }); + describe('_persistableKeys()', function() { + it('should return list of persistable keys', function() { + const config = new Config(); + expect(config._persistableKeys).to.equal(Config._persistableKeys); + }); + }); + describe('addWord()', function() { const config = new Config(Config._defaults); config.words = Object.assign({}, Config._defaultWords); @@ -40,6 +47,14 @@ describe('Config', function() { expect(config.words['newer-word'].sub).to.equal(wordOptions.sub.toLowerCase()); }); + it('should add a new word and not lower substitution when case-sensitive', function() { + const word = 'mountain'; + const sub = 'MoLeHiLl'; + const options = { lists: [], matchMethod: Constants.MATCH_METHODS.EXACT, case: Constants.TRUE, sub: sub }; + expect(config.addWord(word, options)).to.equal(true); + expect(config.words[word].sub).to.equal(sub); + }); + it('should sanitize a new word before adding', function() { expect(config.addWord('anotherNewWord')).to.equal(true); expect(Object.keys(config.words)).to.include('anothernewword'); @@ -62,6 +77,36 @@ describe('Config', function() { }); }); + describe('removeWord()', function() { + let config; + beforeEach(function() { + config = new Config(Config._defaults); + config.words = Object.assign({}, Config._defaultWords); + }); + + it('should remove an existing word', function() { + const wordKey = 'newword'; + config.addWord(wordKey); + expect(config.words[wordKey]).to.exist; + expect(config.removeWord(wordKey)).to.equal(true); + expect(config.words[wordKey]).to.not.exist; + }); + + it('should remove a case-sensitive regex word', function() { + const wordKey = 'newWord'; + config.addWord(wordKey, { lists: [], matchMethod: Constants.MATCH_METHODS.REGEX }); + expect(config.words[wordKey]).to.exist; + expect(config.removeWord(wordKey)).to.equal(true); + expect(config.words[wordKey]).to.not.exist; + }); + + it('should return false when no word to remove', function() { + const wordKey = 'nonexistant'; + expect(config.words[wordKey]).to.not.exist; + expect(config.removeWord(wordKey)).to.equal(false); + }); + }); + describe('repeatForWord()', function() { const config = new Config(Config._defaults); config.words = Object.assign({}, Config._defaultWords); diff --git a/test/spec/lib/constants.spec.ts b/test/spec/lib/constants.spec.ts new file mode 100644 index 00000000..50d105b5 --- /dev/null +++ b/test/spec/lib/constants.spec.ts @@ -0,0 +1,40 @@ +import { expect } from 'chai'; +import Constants from '@APF/lib/constants'; + +describe('Constants', function() { + describe('filterMethodName()', function() { + it('should return filter method name for id', function() { + expect(Constants.filterMethodName(1)).to.equal('Substitute'); + }); + }); + + describe('loggingLevelName()', function() { + it('should return log level name for id', function() { + expect(Constants.loggingLevelName(1)).to.equal('INFO'); + }); + }); + + describe('matchMethodName()', function() { + it('should return match method name for id', function() { + expect(Constants.matchMethodName(1)).to.equal('Partial'); + }); + }); + + describe('nameById()', function() { + it('should return friendly name for id', function() { + expect(Constants.nameById(Constants.FILTER_METHODS, 1)).to.equal('Substitute'); + }); + }); + + describe('nameByValue()', function() { + it('should return name for id', function() { + expect(Constants.nameByValue(Constants.LOGGING_LEVELS, 1)).to.equal('INFO'); + }); + }); + + describe('orderedArray()', function() { + it('should return an ordered array with friendly names of a constants object', function() { + expect(Constants.orderedArray(Constants.FILTER_METHODS)).to.eql(['Censor', 'Substitute', 'Remove', 'Off']); + }); + }); +}); diff --git a/test/spec/lib/filter.spec.ts b/test/spec/lib/filter.spec.ts index 7c5b09a2..513f3542 100644 --- a/test/spec/lib/filter.spec.ts +++ b/test/spec/lib/filter.spec.ts @@ -91,6 +91,24 @@ describe('Filter', () => { }); describe('replaceText()', () => { + describe('Off', () => { + describe('Should still filter even when off if called', () => { + it('preserveFirst', () => { + const filter = new Filter; + filter.cfg = new Config({ + censorCharacter: '*', + censorFixedLength: 0, + filterMethod: Constants.FILTER_METHODS.OFF, + preserveFirst: true, + preserveLast: false, + words: Object.assign({}, testWords), + }); + filter.init(); + expect(filter.replaceText('this is a placeholder placeholder.')).to.equal('this is a p********** p**********.'); + }); + }); + }); + describe('Censor', () => { describe('Exact', () => { it('preserveFirst', () => { diff --git a/test/spec/lib/helper.spec.ts b/test/spec/lib/helper.spec.ts index f2ab771a..b69fe722 100644 --- a/test/spec/lib/helper.spec.ts +++ b/test/spec/lib/helper.spec.ts @@ -7,10 +7,17 @@ import { getVersion, hmsToSeconds, isVersionOlder, + lastElement, numberToBoolean, + numberWithCommas, + prettyPrintArray, + randomArrayElement, removeFromArray, secondsToHMS, + sortObjectKeys, + stringArray, timeForFileName, + upperCaseFirst, } from '@APF/lib/helper'; const array = ['a', 'needle', 'in', 'a', 'large', 'haystack']; @@ -100,6 +107,10 @@ describe('Helper', function() { expect(hmsToSeconds('1:22:17.79')).to.eql(4937.79); expect(hmsToSeconds('3:00:18.500')).to.eql(10818.5); }); + + it('should handle empty input string', function() { + expect(hmsToSeconds('')).to.eql(0); + }); }); describe('isVersionOlder()', function() { @@ -156,6 +167,14 @@ describe('Helper', function() { }); }); + describe('lastElement()', function() { + it('Returns last element in array', function() { + expect(lastElement([1])).to.eql(1); + expect(lastElement([1, 2, 3])).to.eql(3); + expect(lastElement([])).to.eql(undefined); + }); + }); + describe('numberToBoolean()', function() { it('Return a boolean from a number', function() { expect(numberToBoolean(Constants.FALSE)).to.eql(false); @@ -166,11 +185,46 @@ describe('Helper', function() { }); }); + describe('numberWithCommas()', function() { + it('Works with numbers', function() { + expect(numberWithCommas(123)).to.eql('123'); + expect(numberWithCommas(1234)).to.eql('1,234'); + expect(numberWithCommas(1234567890)).to.eql('1,234,567,890'); + }); + + it('Works with number string', function() { + expect(numberWithCommas('123')).to.eql('123'); + expect(numberWithCommas('1234')).to.eql('1,234'); + expect(numberWithCommas('1234567890')).to.eql('1,234,567,890'); + }); + }); + + describe('prettyPrintArray()', function() { + it('Single element', function() { + expect(prettyPrintArray(['abc'])).to.eql('[abc]'); + }); + + it('Multiple element', function() { + expect(prettyPrintArray(['abc', '123', 'zyx'])).to.eql('[abc, 123, zyx]'); + }); + }); + + describe('randomArrayElement()', function() { + it('Returns random item from array', function() { + const values = ['abc', 123]; + expect(randomArrayElement(values)).to.be.oneOf(values); + }); + }); + describe('removeFromArray()', function() { it('should return an array with the matching element removed', function() { expect(removeFromArray(array, 'needle')).to.eql(['a', 'in', 'a', 'large', 'haystack']); }); + it('should return an array with multiple removed values', function() { + expect(removeFromArray(array, ['a', 'needle', 'in'])).to.eql(['large', 'haystack']); + }); + it('should return an array with the same values if no match is found', function() { expect(removeFromArray(array, 'pin')).to.eql(array); }); @@ -191,9 +245,49 @@ describe('Helper', function() { }); }); + describe('sortObjectKeys()', function() { + const object = { c: 3, b: 2, a: 1, _z: 0 }; + it('Sorts object keys ignoring underscore-prefixed keys', function() { + expect(Object.keys(object)).to.eql(['c', 'b', 'a', '_z']); + expect(Object.keys(sortObjectKeys(object, true))).to.eql(['a', 'b', 'c']); + }); + + it('Sorts object keys including underscore-prefixed keys', function() { + expect(Object.keys(sortObjectKeys(object, false))).to.eql(['_z', 'a', 'b', 'c']); + }); + }); + + describe('stringArray()', function() { + it('Ensures array when passed a string', function() { + expect(stringArray('abc')).to.eql(['abc']); + }); + + it('Returns provided array', function() { + expect(stringArray(['abc', 'def'])).to.eql(['abc', 'def']); + }); + }); + describe('timeForFileName()', function() { it('Returns time string', function() { expect(timeForFileName()).to.match(/\d{4}-\d{2}-\d{2}_\d{6}/); }); }); + + describe('upperCaseFirst()', function() { + it('Returns string with first character uppercased', function() { + expect(upperCaseFirst('abc', false)).to.eql('Abc'); + }); + + it('Returns string with first character uppercased and the rest lowercased', function() { + expect(upperCaseFirst('aBC', true)).to.eql('Abc'); + }); + + it('Returns string with first character uppercased and the rest lowercased', function() { + expect(upperCaseFirst('hello WORLD', true)).to.eql('Hello world'); + }); + + it('Returns string with first character uppercased leaving the rest alone', function() { + expect(upperCaseFirst('hello WORLD', false)).to.eql('Hello WORLD'); + }); + }); }); diff --git a/test/spec/lib/logger.spec.ts b/test/spec/lib/logger.spec.ts new file mode 100644 index 00000000..6b69513c --- /dev/null +++ b/test/spec/lib/logger.spec.ts @@ -0,0 +1,262 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import Logger from '@APF/lib/logger'; +import Constants from '@APF/lib/constants'; + +describe('Logger', function() { + + describe('class', function() { + const loggerStubs = {}; + + beforeEach(function() { + for (const level of Object.keys(Constants.LOGGING_LEVELS)) { + loggerStubs[level.toLowerCase()] = sinon.stub(console, level.toLowerCase()); + } + }); + + afterEach(function() { + for (const stub of Object.values(loggerStubs)) { + stub?.restore(); + } + }); + + describe('debug', function() { + it('should output debug message', function() { + Logger.debug('debug message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should output debugTime message', function() { + Logger.debugTime('debugTime message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + describe('with data', function() { + it('should output debug message with data', function() { + Logger.debug('debug message', ['data']); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should output debugTime message with data', function() { + Logger.debugTime('debugTime message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + }); + }); + + describe('error', function() { + it('should output error message', function() { + Logger.error('error message'); + expect(loggerStubs[Logger.errorName].called).to.be.true; + }); + + it('should output errorTime message', function() { + Logger.errorTime('errorTime message'); + expect(loggerStubs[Logger.errorName].called).to.be.true; + }); + }); + + describe('info', function() { + it('should output info message', function() { + Logger.info('info message'); + expect(loggerStubs[Logger.infoName].called).to.be.true; + }); + + it('should output infoTime message', function() { + Logger.infoTime('infoTime message'); + expect(loggerStubs[Logger.infoName].called).to.be.true; + }); + }); + + describe('log()', function() { + it('should output debug message', function() { + Logger.log(Logger.debugName, 'debug message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + }); + + describe('logTime()', function() { + it('should output debug message', function() { + Logger.logTime(Logger.debugName, 'debug message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + }); + + describe('warn', function() { + it('should output warn message', function() { + Logger.warn('warn message'); + expect(loggerStubs[Logger.warnName].called).to.be.true; + }); + + it('should output warnTime message', function() { + Logger.warnTime('warnTime message'); + expect(loggerStubs[Logger.warnName].called).to.be.true; + }); + }); + }); + + describe('instance', function() { + const logger = new Logger(); + const loggerStubs = {}; + + beforeEach(function() { + for (const level of Object.keys(Constants.LOGGING_LEVELS)) { + loggerStubs[level.toLowerCase()] = sinon.stub(console, level.toLowerCase()); + } + }); + + afterEach(function() { + for (const stub of Object.values(loggerStubs)) { + stub?.restore(); + } + }); + + describe('debug', function() { + describe('debug()', function() { + it('should output debug message', function() { + logger.setLevel(Logger.debugLevel); + logger.debug('debug message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should not output debug message', function() { + logger.setLevel(Logger.warnLevel); + logger.debug('debug message'); + expect(loggerStubs[Logger.debugName].called).to.be.false; + }); + + describe('with data', function() { + it('should output debug message with data', function() { + logger.setLevel(Logger.debugLevel); + logger.debug('debug message', ['data']); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should not output debug message with data', function() { + logger.setLevel(Logger.warnLevel); + logger.debug('debug message', ['data']); + expect(loggerStubs[Logger.debugName].called).to.be.false; + }); + }); + }); + + describe('debugTime()', function() { + it('should output debugTime message', function() { + logger.setLevel(Logger.debugLevel); + logger.debugTime('debugTime message'); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should not output debugTime message', function() { + logger.setLevel(Logger.warnLevel); + logger.debugTime('debugTime message'); + expect(loggerStubs[Logger.debugName].called).to.be.false; + }); + + describe('with data', function() { + it('should output debugTime message with data', function() { + logger.setLevel(Logger.debugLevel); + logger.debugTime('debugTime message', ['data']); + expect(loggerStubs[Logger.debugName].called).to.be.true; + }); + + it('should not output debugTime message with data', function() { + logger.setLevel(Logger.warnLevel); + logger.debugTime('debugTime message', ['data']); + expect(loggerStubs[Logger.debugName].called).to.be.false; + }); + }); + }); + }); + + describe('error', function() { + describe('error()', function() { + it('should output error message', function() { + logger.setLevel(Logger.errorLevel); + logger.error('error message'); + expect(loggerStubs[Logger.errorName].called).to.be.true; + }); + + it('should not output error message', function() { + logger.setLevel(100); // Placeholder for higher than error level + logger.error('error message'); + expect(loggerStubs[Logger.errorName].called).to.be.false; + }); + }); + + describe('errorTime()', function() { + it('should output errorTime message', function() { + logger.setLevel(Logger.errorLevel); + logger.errorTime('errorTime message'); + expect(loggerStubs[Logger.errorName].called).to.be.true; + }); + + it('should not output errorTime message', function() { + logger.setLevel(100); // Placeholder for higher than error level + logger.errorTime('errorTime message'); + expect(loggerStubs[Logger.errorName].called).to.be.false; + }); + }); + }); + + describe('info', function() { + describe('info()', function() { + it('should output info message', function() { + logger.setLevel(Logger.infoLevel); + logger.info('info message'); + expect(loggerStubs[Logger.infoName].called).to.be.true; + }); + + it('should not output info message', function() { + logger.setLevel(Logger.warnLevel); + logger.info('info message'); + expect(loggerStubs[Logger.infoName].called).to.be.false; + }); + }); + + describe('infoTime()', function() { + it('should output infoTime message', function() { + logger.setLevel(Logger.infoLevel); + logger.infoTime('infoTime message'); + expect(loggerStubs[Logger.infoName].called).to.be.true; + }); + + it('should not output infoTime message', function() { + logger.setLevel(Logger.warnLevel); + logger.infoTime('infoTime message'); + expect(loggerStubs[Logger.infoName].called).to.be.false; + }); + }); + }); + + describe('warn', function() { + describe('warn()', function() { + it('should output warn message', function() { + logger.setLevel(Logger.warnLevel); + logger.warn('warn message'); + expect(loggerStubs[Logger.warnName].called).to.be.true; + }); + + it('should not output warn message', function() { + logger.setLevel(Logger.errorLevel); + logger.warn('warn message'); + expect(loggerStubs[Logger.warnName].called).to.be.false; + }); + }); + + describe('warnTime()', function() { + it('should output warnTime message', function() { + logger.setLevel(Logger.warnLevel); + logger.warnTime('warnTime message'); + expect(loggerStubs[Logger.warnName].called).to.be.true; + }); + + it('should not output warnTime message', function() { + logger.setLevel(Logger.errorLevel); + logger.warnTime('warnTime message'); + expect(loggerStubs[Logger.warnName].called).to.be.false; + }); + }); + }); + }); +}); diff --git a/test/spec/lib/word.spec.ts b/test/spec/lib/word.spec.ts index 8656a6ee..f59978e4 100644 --- a/test/spec/lib/word.spec.ts +++ b/test/spec/lib/word.spec.ts @@ -59,6 +59,14 @@ describe('Word', function() { ); }); }); + + it('should throw error on invalid regexp', function() { + // Using a function wrapper to test for constructor exceptions + const constructorWrapper = () => { + new Word('$\\z+(^', { matchMethod: Constants.MATCH_METHODS.REGEX }, Config._defaults); + }; + expect(constructorWrapper).to.throw(Error); + }); }); describe('Partial Match', function() { @@ -313,6 +321,10 @@ describe('Word', function() { it('should return false when string does not include a double-byte UTF character', function() { expect(Word.containsDoubleByte('$p[cialC@se')).to.equal(false); }); + + it('should return false on empty string', function() { + expect(Word.containsDoubleByte('')).to.equal(false); + }); }); describe('escapeRegExp()', function() { diff --git a/test/spec/lib/wordlist.spec.ts b/test/spec/lib/wordlist.spec.ts index aefb4acf..2e5be994 100644 --- a/test/spec/lib/wordlist.spec.ts +++ b/test/spec/lib/wordlist.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import Constants from '@APF/lib/constants'; import Wordlist from '@APF/lib/wordlist'; @@ -24,6 +25,16 @@ describe('Wordlist', function() { expect(wordlist.list).to.eql(['placeholder', 'example', 'word']); expect(wordlist.regExps.length).to.equal(3); }); + + it('should report warning on error adding word', function() { + const logSpy = sinon.spy(Wordlist.logger, 'warn'); + const word = '$\\z+(^'; + const words = { [word]: { lists: [1], matchMethod: Constants.MATCH_METHODS.REGEX } }; + const wordlist = new Wordlist({ words: words }, 1); + expect(wordlist.regExps.length).to.equal(0); + expect(logSpy.callCount).to.equal(1); + expect(logSpy.firstCall.args[0]).to.equal(`Failed to add '${word}' to wordlist.`); + }); }); describe('find()', function() { @@ -37,6 +48,11 @@ describe('Wordlist', function() { expect(wordlist.find(1).value).to.equal('sample'); }); + it('return undefined with null', function() { + const wordlist = new Wordlist({ words: testWords }, 2); + expect(wordlist.find(null)).to.equal(undefined); + }); + it('non-existent word', function() { const wordlist = new Wordlist({ words: testWords }, 2); expect(wordlist.find(99)).to.equal(undefined);