commit 1179c4b1da1082a6a3557d7e1933f61d22a40789 Author: Pascal Perrenoud Date: Sun May 19 22:47:42 2024 +0200 Validation Package, implementation and tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ede773 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +bun.lockb +package-lock.json +node_modules/ +._* +.DS_Store diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..98f3d83 --- /dev/null +++ b/index.test.ts @@ -0,0 +1,83 @@ +import {describe, expect, test} from 'bun:test' + +import * as f from '.' + +// TODO : Test logging + +describe('email', () => { + const emails: [string, boolean][] = [ + ['pascal@pband.ch', true], + ['p@p.ch', true], + [`${'a'.repeat(64)}@${'b'.repeat(60)}.ch`, true], + [`${'a'.repeat(128)}@${'b'.repeat(64)}.ch`, false], + ['google.ch@test.com', true], + ['pascal+truc@pband.ch', true], + + ['pascal+.@pband.ch', false], + ['pascal+p.@pband.ch', false], + ['pascal@pband.', false], + ['pascal@pband', false], + ['pascal@.ch', false], + ['@pband.ch', false], + ['prout', false], + ['google.ch', false], + ] + + for (const [email, valid] of emails) { + test(`"${email}" is ${valid ? 'valid' : 'invalid'}`, () => { + const res = expect(f.email_is_valid(email)) + if (valid) { + res.toBeTrue() + } else { + res.toBeFalse() + } + }) + } +}) +describe('Password strength', () => { + // TODO : More passwords lol + const pwds: [string, f.PasswordStrength][] = [ + // ['test', f.PasswordStrength.Strong], + ['OkPassword', f.PasswordStrength.Easy], + ] + + for (const pwd of pwds) { + for (const level of [f.PasswordStrength.Weak, f.PasswordStrength.Easy, f.PasswordStrength.Medium, f.PasswordStrength.Strong, f.PasswordStrength.Hard]) { + const should_pass = level <= pwd[1] + test(`"${pwd[0]}" should ${should_pass ? '' : 'not '}pass level ${level}`, () => { + const res = expect(f.password_is_strong(pwd[0], level as f.PasswordStrength)) + if (should_pass) { + res.toBeTrue() + } else { + res.toBeFalse() + } + }) + } + } +}) +describe('Contains HTML', () => { + const texts: [string, boolean][] = [ + ['Hello, world!', false], + ['

Hello, world!

', true], + ['

Hello, world!

', true], + ['Hello guy !', true], + ['Hello guy !', true], + ['Simple balise
', true], + ['Simple balise
', true], + ['Simple balise
', true], + ['Simple math : 1 < 2', false], + ['Simple math : 1 < 2 > 0', false], + ] + + for (const i in texts) { + const [text, contains] = texts[i] + test(`Text ${i} : should ${contains ? '' : 'not '}contain HTML`, () => { + const res = expect(f.contains_html(text)) + if (contains) { + res.toBeTrue() + } else { + res.toBeFalse() + } + }) + } +}) diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..ee17572 --- /dev/null +++ b/index.ts @@ -0,0 +1,52 @@ +import * as EmailValidator from 'email-validator' +import logger from 'log' +import zxcvbn from 'zxcvbn' +import {parseDocument} from 'htmlparser2' + +let _verbose: boolean = false +export function verbose(v: boolean) { + _verbose = v +} + +const log = logger('validation') + +export enum PasswordStrength { + Weak = 0, + Easy = 1, + Medium = 2, + Strong = 3, + Hard = 4, +} + +export function password_is_strong(password: string, strength: PasswordStrength = PasswordStrength.Medium, email?: string) : boolean { + log.trace("Verify password's strength") + + if (zxcvbn(password, email !== undefined ? [email] : []).score < strength) { + if (_verbose) log.warn("Password is too weak") + return false + } + + return true +} + +export function contains_html(text: string) : boolean { + log.trace("Verify if text contains HTML") + + if (parseDocument(text).children.some(node => node.nodeType === 1)) { + if (_verbose) log.warn("") + return true + } + + return false +} + +export function email_is_valid(email: string) : boolean { + log.trace("Verify email's validity") + + if (!EmailValidator.validate(email)) { + if (_verbose) log.warn("Invalid email") + return false + } + + return true +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7ff288 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "validation", + "description": "Various validation for web usage", + "version": "1.0.0", + + "author": "Pascal Perrenoud ", + "module": "index.ts", + "type": "module", + "files": ["index.ts"], + + "scripts": { + "test": "bun test" + }, + + "dependencies": { + "email-validator": "^2.0.4", + "htmlparser2": "^9.1.0", + "log": "git+https://git.pband.ch/typescript/log", + "zxcvbn": "^4.4.2" + }, + "devDependencies": { + "@types/bun": "^1.1.2", + "@types/zxcvbn": "^4.4.4" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ba96c23 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + + // Enable latest features + "lib": ["ESNext","dom"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + "checkJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "forceConsistentCasingInFileNames": true, + "noPropertyAccessFromIndexSignature": false + }, + + "include": [ + "index.ts", + "src/**/*.ts" + ] +}