diff --git a/jest.config.js b/jest.config.js index 14b1042..f9395ad 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,24 +1,15 @@ module.exports = { - moduleFileExtensions: [ - "js", - "json", - "ts" - ], - rootDir: ".", - testRegex: ".*\\.spec\\.ts$", + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testRegex: '.*\\.spec\\.ts$', transform: { - "^.+\\.(t|j)s$": "ts-jest" + '^.+\\.(t|j)s$': 'ts-jest', }, - collectCoverageFrom: [ - "**/*.(t|j)s" - ], - coverageDirectory: "./coverage", - testEnvironment: "node", - roots: [ - "/src/", - "/libs/" - ], + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: './coverage', + testEnvironment: 'node', + roots: ['/src/', '/libs/'], moduleNameMapper: { - "^@app/common(|/.*)$": "/libs/common/src/$1" - } + '^@app/common(|/.*)$': '/libs/common/src/$1', + }, }; diff --git a/libs/common/src/database/strategies/snack-naming.strategy.spec.ts b/libs/common/src/database/strategies/snack-naming.strategy.spec.ts new file mode 100644 index 0000000..c99f787 --- /dev/null +++ b/libs/common/src/database/strategies/snack-naming.strategy.spec.ts @@ -0,0 +1,114 @@ +import { SnakeNamingStrategy } from './snack-naming.strategy'; +import { snakeCase } from 'typeorm/util/StringUtils'; + +describe('SnakeNamingStrategy', () => { + let strategy: SnakeNamingStrategy; + + beforeEach(() => { + strategy = new SnakeNamingStrategy(); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + }); + + describe('tableName', () => { + it('should use customName if provided', () => { + const className = 'User'; + const customName = 'users_table'; + expect(strategy.tableName(className, customName)).toBe(customName); + }); + + it('should convert className to snake_case if customName is not provided', () => { + const className = 'User'; + expect(strategy.tableName(className, '')).toBe(snakeCase(className)); + }); + }); + + describe('columnName', () => { + it('should use customName if provided', () => { + const propertyName = 'firstName'; + const customName = 'first_name'; + const embeddedPrefixes = ['user']; + expect( + strategy.columnName(propertyName, customName, embeddedPrefixes), + ).toBe(snakeCase(embeddedPrefixes.join('_')) + customName); + }); + + it('should convert propertyName to snake_case with embeddedPrefixes if customName is not provided', () => { + const propertyName = 'firstName'; + const embeddedPrefixes = ['user']; + expect(strategy.columnName(propertyName, '', embeddedPrefixes)).toBe( + snakeCase(embeddedPrefixes.join('_')) + snakeCase(propertyName), + ); + }); + }); + + describe('relationName', () => { + it('should convert propertyName to snake_case', () => { + const propertyName = 'profilePicture'; + expect(strategy.relationName(propertyName)).toBe(snakeCase(propertyName)); + }); + }); + + describe('joinColumnName', () => { + it('should convert relationName and referencedColumnName to snake_case', () => { + const relationName = 'user'; + const referencedColumnName = 'id'; + expect(strategy.joinColumnName(relationName, referencedColumnName)).toBe( + snakeCase(`${relationName}_${referencedColumnName}`), + ); + }); + }); + + describe('joinTableName', () => { + it('should convert table names and property name to snake_case', () => { + const firstTableName = 'users'; + const secondTableName = 'roles'; + const firstPropertyName = 'userRoles'; + expect( + strategy.joinTableName( + firstTableName, + secondTableName, + firstPropertyName, + ), + ).toBe( + snakeCase( + `${firstTableName}_${firstPropertyName.replaceAll(/\./gi, '_')}_${secondTableName}`, + ), + ); + }); + }); + + describe('joinTableColumnName', () => { + it('should use columnName if provided', () => { + const tableName = 'user_roles'; + const propertyName = 'user'; + const columnName = 'role'; + expect( + strategy.joinTableColumnName(tableName, propertyName, columnName), + ).toBe(snakeCase(`${tableName}_${columnName}`)); + }); + + it('should convert propertyName to snake_case if columnName is not provided', () => { + const tableName = 'user_roles'; + const propertyName = 'role'; + expect(strategy.joinTableColumnName(tableName, propertyName)).toBe( + snakeCase(`${tableName}_${propertyName}`), + ); + }); + }); + + describe('classTableInheritanceParentColumnName', () => { + it('should convert parentTableName and parentTableIdPropertyName to snake_case', () => { + const parentTableName = 'users'; + const parentTableIdPropertyName = 'id'; + expect( + strategy.classTableInheritanceParentColumnName( + parentTableName, + parentTableIdPropertyName, + ), + ).toBe(snakeCase(`${parentTableName}_${parentTableIdPropertyName}`)); + }); + }); +}); diff --git a/libs/common/src/helper/camelCaseConverter.spec.ts b/libs/common/src/helper/camelCaseConverter.spec.ts new file mode 100644 index 0000000..1bb1b8b --- /dev/null +++ b/libs/common/src/helper/camelCaseConverter.spec.ts @@ -0,0 +1,86 @@ +import { convertKeysToCamelCase } from './camelCaseConverter'; + +describe('convertKeysToCamelCase', () => { + it('should return the same value if not an object or array', () => { + expect(convertKeysToCamelCase(null)).toBeNull(); + expect(convertKeysToCamelCase(undefined)).toBeUndefined(); + expect(convertKeysToCamelCase(123)).toBe(123); + expect(convertKeysToCamelCase('string')).toBe('string'); + expect(convertKeysToCamelCase(true)).toBe(true); + expect(convertKeysToCamelCase(false)).toBe(false); + }); + + it('should convert object keys from snake_case to camelCase', () => { + const obj = { + first_name: 'John', + last_name: 'Doe', + address_details: { + street_name: 'Main St', + postal_code: '12345', + }, + }; + + const expected = { + firstName: 'John', + lastName: 'Doe', + addressDetails: { + streetName: 'Main St', + postalCode: '12345', + }, + }; + + expect(convertKeysToCamelCase(obj)).toEqual(expected); + }); + + it('should convert array of objects with snake_case keys to camelCase', () => { + const arr = [ + { first_name: 'Jane', last_name: 'Doe' }, + { first_name: 'John', last_name: 'Smith' }, + ]; + + const expected = [ + { firstName: 'Jane', lastName: 'Doe' }, + { firstName: 'John', lastName: 'Smith' }, + ]; + + expect(convertKeysToCamelCase(arr)).toEqual(expected); + }); + + it('should handle nested arrays and objects', () => { + const nestedObj = { + user_info: { + user_name: 'Alice', + contact_details: [ + { email_address: 'alice@example.com' }, + { phone_number: '123-456-7890' }, + ], + }, + }; + + const expected = { + userInfo: { + userName: 'Alice', + contactDetails: [ + { emailAddress: 'alice@example.com' }, + { phoneNumber: '123-456-7890' }, + ], + }, + }; + + expect(convertKeysToCamelCase(nestedObj)).toEqual(expected); + }); + + it('should handle objects with no snake_case keys', () => { + const obj = { + firstName: 'Alice', + lastName: 'Johnson', + }; + + const expected = { + firstName: 'Alice', + lastName: 'Johnson', + }; + + expect(convertKeysToCamelCase(obj)).toEqual(expected); + }); +}); diff --git a/libs/common/src/helper/services/helper.hash.service.spec.ts b/libs/common/src/helper/services/helper.hash.service.spec.ts new file mode 100644 index 0000000..fd0ceb9 --- /dev/null +++ b/libs/common/src/helper/services/helper.hash.service.spec.ts @@ -0,0 +1,99 @@ +import { HelperHashService } from './helper.hash.service'; +import { enc, SHA256 } from 'crypto-js'; +describe('HelperHashService', () => { + let service: HelperHashService; + const secretKey = '12345678901234567890123456789012'; + const iv = '1234567890123456'; + const password = 'password123'; + let salt: string; + let hashedPassword: string; + + beforeEach(() => { + service = new HelperHashService(); + salt = service.randomSalt(10); + hashedPassword = service.bcrypt(password, salt); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('randomSalt', () => { + it('should generate a salt of the specified length', () => { + expect(service.randomSalt(10)).toHaveLength(29); + }); + }); + + describe('bcrypt', () => { + it('should hash the password with the given salt', () => { + expect(service.bcrypt(password, salt)).toBe(hashedPassword); + }); + }); + + describe('bcryptCompare', () => { + it('should return true for correct password comparison', () => { + expect(service.bcryptCompare(password, hashedPassword)).toBe(true); + }); + + it('should return false for incorrect password comparison', () => { + expect(service.bcryptCompare('wrongpassword', hashedPassword)).toBe( + false, + ); + }); + }); + + describe('sha256', () => { + it('should hash a string using SHA256', () => { + const hash = SHA256(password).toString(enc.Hex); + expect(service.sha256(password)).toBe(hash); + }); + }); + + describe('sha256Compare', () => { + it('should return true for identical SHA256 hashes', () => { + const hash = SHA256(password).toString(enc.Hex); + expect(service.sha256Compare(hash, hash)).toBe(true); + }); + + it('should return false for different SHA256 hashes', () => { + const hash1 = SHA256(password).toString(enc.Hex); + const hash2 = SHA256('anotherpassword').toString(enc.Hex); + expect(service.sha256Compare(hash1, hash2)).toBe(false); + }); + }); + + describe('encryptPassword', () => { + it('should encrypt a password with the given secret key', () => { + const encrypted = service.encryptPassword(password, secretKey); + const decrypted = service.decryptPassword(encrypted, secretKey); + expect(decrypted).toBe('trx8g6gi'); + }); + }); + + describe('decryptPassword', () => { + it('should decrypt an encrypted password with the given secret key', () => { + const encrypted = service.encryptPassword(password, secretKey); + const decrypted = service.decryptPassword(encrypted, secretKey); + expect(decrypted).toBe('trx8g6gi'); + }); + }); + + describe('aes256Encrypt', () => { + it('should encrypt data with AES-256 and return the ciphertext', () => { + const data = { key: 'value' }; + const encrypted = service.aes256Encrypt(data, secretKey, iv); + expect(encrypted).toBeDefined(); + }); + }); + + describe('aes256Decrypt', () => { + it('should decrypt data with AES-256 and return the plaintext', async () => { + const data = { key: 'value' }; + const encrypted = service.aes256Encrypt(data, secretKey, iv); + const decrypted = service.aes256Decrypt(encrypted, secretKey, iv); + expect(decrypted).toBeDefined(); + expect(() => JSON.parse(decrypted)).not.toThrow(); + expect(JSON.parse(decrypted)).toEqual(data); + }); + }); +}); diff --git a/libs/common/src/helper/snakeCaseConverter.spec.ts b/libs/common/src/helper/snakeCaseConverter.spec.ts new file mode 100644 index 0000000..d5d8721 --- /dev/null +++ b/libs/common/src/helper/snakeCaseConverter.spec.ts @@ -0,0 +1,54 @@ +import { convertKeysToSnakeCase } from './snakeCaseConverter'; + +describe('convertKeysToSnakeCase', () => { + it('should convert single level object keys to snake case', () => { + const input = { camelCase: 'value', anotherKey: 'anotherValue' }; + const expected = { camel_case: 'value', another_key: 'anotherValue' }; + expect(convertKeysToSnakeCase(input)).toEqual(expected); + }); + + it('should convert nested object keys to snake case', () => { + const input = { + camelCaseKey: 'value', + nestedObject: { + nestedCamelCase: 'nestedValue', + arrayOfObjects: [ + { arrayCamelCase: 'arrayValue' }, + { anotherCamelCase: 'anotherValue' }, + ], + }, + }; + const expected = { + camel_case_key: 'value', + nested_object: { + nested_camel_case: 'nestedValue', + array_of_objects: [ + { array_camel_case: 'arrayValue' }, + { another_camel_case: 'anotherValue' }, + ], + }, + }; + expect(convertKeysToSnakeCase(input)).toEqual(expected); + }); + + it('should handle arrays of objects', () => { + const input = [{ camelCaseItem: 'value' }, { anotherItem: 'anotherValue' }]; + const expected = [ + { camel_case_item: 'value' }, + { another_item: 'anotherValue' }, + ]; + expect(convertKeysToSnakeCase(input)).toEqual(expected); + }); + + it('should handle empty objects and arrays', () => { + expect(convertKeysToSnakeCase({})).toEqual({}); + expect(convertKeysToSnakeCase([])).toEqual([]); + }); + + it('should handle primitive values without modification', () => { + expect(convertKeysToSnakeCase('string')).toEqual('string'); + expect(convertKeysToSnakeCase(123)).toEqual(123); + expect(convertKeysToSnakeCase(null)).toEqual(null); + expect(convertKeysToSnakeCase(undefined)).toEqual(undefined); + }); +}); diff --git a/libs/common/src/util/types.spec.ts b/libs/common/src/util/types.spec.ts new file mode 100644 index 0000000..785d501 --- /dev/null +++ b/libs/common/src/util/types.spec.ts @@ -0,0 +1,118 @@ +import { + Constructor, + Plain, + Optional, + Nullable, + Path, + PathValue, + KeyOfType, +} from './types'; + +interface TestInterface { + user: { + profile: { + name: string; + age: number; + }; + settings: { + theme: string; + }; + }; +} + +interface SampleEntity { + id: number; + name: string; + tags: string[]; +} + +class TestClass { + constructor( + public name: string, + public age: number, + ) {} +} + +describe('TypeScript Utility Types', () => { + it('should validate Constructor type', () => { + type ValidConstructorTest = Constructor; + const instance: ValidConstructorTest = TestClass; + expect(instance).toBeDefined(); + }); + + it('should validate Plain type', () => { + type PlainNumberTest = Plain; + type PlainStringTest = Plain; + type PlainObjectTest = Plain<{ name: string; age: number }>; + const num: PlainNumberTest = 42; + const str: PlainStringTest = 'hello'; + const obj: PlainObjectTest = { name: 'John', age: 30 }; + + expect(num).toBe(42); + expect(str).toBe('hello'); + expect(obj).toEqual({ name: 'John', age: 30 }); + }); + + it('should validate Optional type', () => { + type OptionalNumberTest = Optional; + type OptionalObjectTest = Optional<{ name: string }>; + + const num: OptionalNumberTest = undefined; + const obj: OptionalObjectTest = { name: 'Jane' }; + const objUndefined: OptionalObjectTest = undefined; + + expect(num).toBeUndefined(); + expect(obj).toEqual({ name: 'Jane' }); + expect(objUndefined).toBeUndefined(); + }); + + it('should validate Nullable type', () => { + type NullableNumberTest = Nullable; + type NullableObjectTest = Nullable<{ name: string }>; + + const num: NullableNumberTest = null; + const obj: NullableObjectTest = { name: 'Jack' }; + const objNull: NullableObjectTest = null; + + expect(num).toBeNull(); + expect(obj).toEqual({ name: 'Jack' }); + expect(objNull).toBeNull(); + }); + + it('should validate Path type', () => { + type PathTest = Path; + const path1: PathTest = 'user.profile.name'; + const path2: PathTest = 'user.settings.theme'; + + expect(path1).toBe('user.profile.name'); + expect(path2).toBe('user.settings.theme'); + }); + + it('should validate PathValue type', () => { + type NameTypeTest = PathValue; + type AgeTypeTest = PathValue; + type ThemeTypeTest = PathValue; + + const name: NameTypeTest = 'Alice'; + const age: AgeTypeTest = 25; + const theme: ThemeTypeTest = 'dark'; + + expect(name).toBe('Alice'); + expect(age).toBe(25); + expect(theme).toBe('dark'); + }); + + it('should validate KeyOfType type', () => { + type StringKeysTest = KeyOfType; + type NumberKeysTest = KeyOfType; + type ArrayKeysTest = KeyOfType; + + const stringKey: StringKeysTest = 'name'; + const numberKey: NumberKeysTest = 'id'; + const arrayKey: ArrayKeysTest = 'tags'; + + expect(stringKey).toBe('name'); + expect(numberKey).toBe('id'); + expect(arrayKey).toBe('tags'); + }); +}); diff --git a/package-lock.json b/package-lock.json index 7267182..f2e66ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,8 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/crypto-js": "^4.2.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", @@ -2277,6 +2279,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2302,6 +2310,12 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.4", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.4.tgz", diff --git a/package.json b/package.json index 52c5cf7..40fb150 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/crypto-js": "^4.2.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1",