core-services-sdk 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,73 +1,3 @@
1
- // @ts-nocheck
2
- import { mongoConnect } from './connect.js'
3
-
4
- /**
5
- * Initializes MongoDB collections and provides a transaction wrapper and read-only client accessor.
6
- *
7
- * @param {Object} options
8
- * @param {{ uri: string, options: { dbName: string } }} options.config - MongoDB connection config
9
- * @param {Record<string, string>} options.collectionNames - Map of collection keys to MongoDB collection names
10
- *
11
- * @returns {Promise<
12
- * Record<string, import('mongodb').Collection> & {
13
- * withTransaction: (action: ({ session: import('mongodb').ClientSession }) => Promise<void>) => Promise<void>,
14
- * readonly client: import('mongodb').MongoClient
15
- * }
16
- * >}
17
- *
18
- * @example
19
- * const { users, logs, withTransaction, client } = await initializeMongoDb({
20
- * config: {
21
- * uri: 'mongodb://localhost:27017',
22
- * options: { dbName: 'mydb' },
23
- * },
24
- * collectionNames: {
25
- * users: 'users',
26
- * logs: 'system_logs',
27
- * },
28
- * });
29
- *
30
- * await withTransaction(async ({ session }) => {
31
- * await users.insertOne({ name: 'Alice' }, { session });
32
- * await logs.insertOne({ event: 'UserCreated', user: 'Alice' }, { session });
33
- * });
34
- *
35
- * await client.close(); // Close connection manually
36
- */
37
- export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
38
- const client = await mongoConnect(config)
39
- const db = client.db(config.options.dbName)
40
-
41
- const collectionRefs = Object.entries(collectionNames).reduce(
42
- (collections, [key, collectionName]) => ({
43
- ...collections,
44
- [key]: db.collection(collectionName),
45
- }),
46
- {},
47
- )
48
-
49
- const withTransaction = async (action) => {
50
- const session = client.startSession()
51
- let actionResponse
52
- try {
53
- session.startTransaction()
54
- actionResponse = await action({ session })
55
- await session.commitTransaction()
56
- } catch (error) {
57
- await session.abortTransaction()
58
- throw error
59
- } finally {
60
- await session.endSession()
61
- return actionResponse
62
- }
63
- }
64
-
65
- return {
66
- ...collectionRefs,
67
- withTransaction,
68
- /** @type {import('mongodb').MongoClient} */
69
- get client() {
70
- return client
71
- },
72
- }
73
- }
1
+ export * from './connect.js'
2
+ export * from './initialize-mongodb.js'
3
+ export * from './validate-mongo-uri.js'
@@ -0,0 +1,73 @@
1
+ // @ts-nocheck
2
+ import { mongoConnect } from './connect.js'
3
+
4
+ /**
5
+ * Initializes MongoDB collections and provides a transaction wrapper and read-only client accessor.
6
+ *
7
+ * @param {Object} options
8
+ * @param {{ uri: string, options: { dbName: string } }} options.config - MongoDB connection config
9
+ * @param {Record<string, string>} options.collectionNames - Map of collection keys to MongoDB collection names
10
+ *
11
+ * @returns {Promise<
12
+ * Record<string, import('mongodb').Collection> & {
13
+ * withTransaction: (action: ({ session: import('mongodb').ClientSession }) => Promise<void>) => Promise<void>,
14
+ * readonly client: import('mongodb').MongoClient
15
+ * }
16
+ * >}
17
+ *
18
+ * @example
19
+ * const { users, logs, withTransaction, client } = await initializeMongoDb({
20
+ * config: {
21
+ * uri: 'mongodb://localhost:27017',
22
+ * options: { dbName: 'mydb' },
23
+ * },
24
+ * collectionNames: {
25
+ * users: 'users',
26
+ * logs: 'system_logs',
27
+ * },
28
+ * });
29
+ *
30
+ * await withTransaction(async ({ session }) => {
31
+ * await users.insertOne({ name: 'Alice' }, { session });
32
+ * await logs.insertOne({ event: 'UserCreated', user: 'Alice' }, { session });
33
+ * });
34
+ *
35
+ * await client.close(); // Close connection manually
36
+ */
37
+ export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
38
+ const client = await mongoConnect(config)
39
+ const db = client.db(config.options.dbName)
40
+
41
+ const collectionRefs = Object.entries(collectionNames).reduce(
42
+ (collections, [key, collectionName]) => ({
43
+ ...collections,
44
+ [key]: db.collection(collectionName),
45
+ }),
46
+ {},
47
+ )
48
+
49
+ const withTransaction = async (action) => {
50
+ const session = client.startSession()
51
+ let actionResponse
52
+ try {
53
+ session.startTransaction()
54
+ actionResponse = await action({ session })
55
+ await session.commitTransaction()
56
+ } catch (error) {
57
+ await session.abortTransaction()
58
+ throw error
59
+ } finally {
60
+ await session.endSession()
61
+ return actionResponse
62
+ }
63
+ }
64
+
65
+ return {
66
+ ...collectionRefs,
67
+ withTransaction,
68
+ /** @type {import('mongodb').MongoClient} */
69
+ get client() {
70
+ return client
71
+ },
72
+ }
73
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Validates whether a given string is a properly formatted MongoDB URI.
3
+ *
4
+ * Supports both standard (`mongodb://`) and SRV (`mongodb+srv://`) protocols.
5
+ *
6
+ * @param {string} uri - The URI string to validate.
7
+ * @returns {boolean} `true` if the URI is a valid MongoDB connection string, otherwise `false`.
8
+ *
9
+ * @example
10
+ * isValidMongoUri('mongodb://localhost:27017/mydb') // true
11
+ * isValidMongoUri('mongodb+srv://cluster.example.com/test') // true
12
+ * isValidMongoUri('http://localhost') // false
13
+ * isValidMongoUri('') // false
14
+ */
15
+ export function isValidMongoUri(uri) {
16
+ if (typeof uri !== 'string' || !uri.trim()) {
17
+ return false
18
+ }
19
+
20
+ try {
21
+ const parsed = new URL(uri)
22
+
23
+ const isValidProtocol =
24
+ parsed.protocol === 'mongodb:' || parsed.protocol === 'mongodb+srv:'
25
+
26
+ const hasHostname = Boolean(parsed.hostname)
27
+
28
+ return isValidProtocol && hasHostname
29
+ } catch (e) {
30
+ return false
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { combineUniqueArrays } from '../../src/core/combine-unique-arrays.js'
4
+
5
+ describe('combineUniqueArrays', () => {
6
+ it('should combine arrays and remove duplicates (numbers)', () => {
7
+ const result = combineUniqueArrays([1, 2], [2, 3], [3, 4])
8
+ expect(result).toEqual([1, 2, 3, 4])
9
+ })
10
+
11
+ it('should combine arrays and remove duplicates (strings)', () => {
12
+ const result = combineUniqueArrays(['a', 'b'], ['b', 'c'])
13
+ expect(result).toEqual(['a', 'b', 'c'])
14
+ })
15
+
16
+ it('should return empty array when no input is provided', () => {
17
+ const result = combineUniqueArrays()
18
+ expect(result).toEqual([])
19
+ })
20
+
21
+ it('should handle single array', () => {
22
+ const result = combineUniqueArrays([1, 2, 2, 3])
23
+ expect(result).toEqual([1, 2, 3])
24
+ })
25
+
26
+ it('should preserve order of first appearance', () => {
27
+ const result = combineUniqueArrays(['b', 'a'], ['a', 'c'])
28
+ expect(result).toEqual(['b', 'a', 'c'])
29
+ })
30
+
31
+ it('should handle mixed types', () => {
32
+ const result = combineUniqueArrays(['1', 1, true, 'true'], [true, '1'])
33
+ expect(result).toEqual(['1', 1, true, 'true'])
34
+ })
35
+ })
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { normalizeToArray } from '../../src/core/normalize-to-array.js'
4
+
5
+ describe('normalizeToArray', () => {
6
+ it('should return an empty array for undefined', () => {
7
+ expect(normalizeToArray(undefined)).toEqual([])
8
+ })
9
+
10
+ it('should return an empty array for null', () => {
11
+ expect(normalizeToArray(null)).toEqual([])
12
+ })
13
+
14
+ it('should split a comma-separated string and trim each value', () => {
15
+ expect(normalizeToArray('a,b, c , ,d')).toEqual(['a', 'b', 'c', 'd'])
16
+ })
17
+
18
+ it('should handle string with extra commas and spaces', () => {
19
+ expect(normalizeToArray(' , a , ,b,, ,c ,')).toEqual(['a', 'b', 'c'])
20
+ })
21
+
22
+ it('should trim and filter empty values from an array of strings', () => {
23
+ expect(normalizeToArray([' a ', 'b', '', ' ', 'c'])).toEqual([
24
+ 'a',
25
+ 'b',
26
+ 'c',
27
+ ])
28
+ })
29
+
30
+ it('should convert non-string/non-array values to string and split by comma', () => {
31
+ expect(normalizeToArray(123)).toEqual(['123'])
32
+ expect(normalizeToArray(true)).toEqual(['true'])
33
+ expect(normalizeToArray({})).toEqual(['[object Object]'])
34
+ })
35
+
36
+ it('should handle an array of non-string values', () => {
37
+ expect(normalizeToArray([1, ' 2 ', null, undefined, true])).toEqual(['2'])
38
+ })
39
+
40
+ it('should preserve order of values after normalization', () => {
41
+ expect(normalizeToArray([' b ', 'a', ' c '])).toEqual(['b', 'a', 'c'])
42
+ })
43
+ })
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import {
4
+ generateCode,
5
+ generateCodeAlpha,
6
+ generateCodeDigits,
7
+ generateCodeAlphaNumeric,
8
+ generateCodeAlphaNumericSymbols,
9
+ OTP_GENERATOR_TYPES,
10
+ } from '../../src/core/otp-generators.js'
11
+
12
+ const charsetForType = {
13
+ [OTP_GENERATOR_TYPES.numeric]: /^[0-9]+$/,
14
+ [OTP_GENERATOR_TYPES.alpha]: /^[a-zA-Z]+$/,
15
+ [OTP_GENERATOR_TYPES.symbols]: /^[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]+$/,
16
+ [OTP_GENERATOR_TYPES.alphaLower]: /^[a-z]+$/,
17
+ [OTP_GENERATOR_TYPES.alphaUpper]: /^[A-Z]+$/,
18
+ [OTP_GENERATOR_TYPES.alphanumeric]: /^[a-zA-Z0-9]+$/,
19
+ [OTP_GENERATOR_TYPES.alphanumericSymbols]:
20
+ /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{}|;:,.<>?]+$/,
21
+ [OTP_GENERATOR_TYPES.any]: /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{}|;:,.<>?]+$/,
22
+ }
23
+
24
+ describe('generateCode', () => {
25
+ it('generates code with correct length', () => {
26
+ const code = generateCode({ length: 10, type: 'numeric' })
27
+ expect(code).toHaveLength(10)
28
+ })
29
+
30
+ it('uses fallback to default type when type is not provided', () => {
31
+ const code = generateCode()
32
+ expect(code).toMatch(/^[0-9]+$/)
33
+ })
34
+
35
+ it('throws error for invalid length', () => {
36
+ expect(() => generateCode({ length: 0 })).toThrow(/length must be a number/)
37
+ expect(() => generateCode({ length: 100 })).toThrow(
38
+ /length must be a number/,
39
+ )
40
+ })
41
+
42
+ it('throws error for invalid charset (non-string)', () => {
43
+ // @ts-ignore
44
+ expect(() => generateCode({ charset: 123 })).toThrow(
45
+ /charset must be a string/,
46
+ )
47
+ })
48
+
49
+ it('throws error for empty charset', () => {
50
+ expect(() => generateCode({ charset: '' })).toThrow(
51
+ /charset must not be empty/,
52
+ )
53
+ })
54
+
55
+ it('throws error for unknown type', () => {
56
+ expect(() => generateCode({ type: 'unknown_type' })).toThrow(
57
+ /type must be one of:/,
58
+ )
59
+ })
60
+
61
+ it('respects custom charset', () => {
62
+ const code = generateCode({ length: 5, charset: 'ABC' })
63
+ expect(code).toMatch(/^[ABC]+$/)
64
+ expect(code.length).toBe(5)
65
+ })
66
+
67
+ for (const [type, regex] of Object.entries(charsetForType)) {
68
+ it(`generates ${type} OTP correctly`, () => {
69
+ const code = generateCode({ length: 8, type })
70
+ expect(code).toMatch(regex)
71
+ })
72
+ }
73
+ })
74
+
75
+ describe('Shortcuts', () => {
76
+ it('generateCodeAlpha produces alphabetic code', () => {
77
+ expect(generateCodeAlpha(6)).toMatch(/^[a-zA-Z]{6}$/)
78
+ })
79
+
80
+ it('generateCodeDigits produces numeric code', () => {
81
+ expect(generateCodeDigits(6)).toMatch(/^[0-9]{6}$/)
82
+ })
83
+
84
+ it('generateCodeAlphaNumeric produces alphanumeric code', () => {
85
+ expect(generateCodeAlphaNumeric(6)).toMatch(/^[a-zA-Z0-9]{6}$/)
86
+ })
87
+
88
+ it('generateCodeAlphaNumericSymbols includes symbols', () => {
89
+ expect(generateCodeAlphaNumericSymbols(6)).toMatch(
90
+ /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{}|;:,.<>?]{6}$/,
91
+ )
92
+ })
93
+ })
@@ -0,0 +1,41 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ import { isValidRegex } from '../../src/core/regex-utils.js'
5
+
6
+ describe('isValidRegex', () => {
7
+ it('returns true for valid regex strings', () => {
8
+ expect(isValidRegex('^[a-z]+$')).toBe(true)
9
+ expect(isValidRegex('\\d{3}-\\d{2}-\\d{4}')).toBe(true)
10
+ expect(isValidRegex('^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$')).toBe(true)
11
+ expect(isValidRegex('.*')).toBe(true)
12
+ expect(isValidRegex('^$')).toBe(true)
13
+ })
14
+
15
+ it('returns true for RegExp objects', () => {
16
+ expect(isValidRegex(/^[a-z]+$/)).toBe(true)
17
+ expect(isValidRegex(/\d{3}/)).toBe(true)
18
+ expect(isValidRegex(/abc/i)).toBe(true)
19
+ })
20
+
21
+ it('returns false for invalid regex strings', () => {
22
+ expect(isValidRegex('[')).toBe(false)
23
+ expect(isValidRegex('(')).toBe(false)
24
+ expect(isValidRegex('\\')).toBe(false)
25
+ expect(isValidRegex('[a-z')).toBe(false)
26
+ expect(isValidRegex('(abc')).toBe(false)
27
+ })
28
+
29
+ it('returns false for invalid types', () => {
30
+ expect(isValidRegex(undefined)).toBe(false)
31
+ expect(isValidRegex(null)).toBe(false)
32
+ expect(isValidRegex(123)).toBe(false)
33
+ expect(isValidRegex({})).toBe(false)
34
+ expect(isValidRegex([])).toBe(false)
35
+ expect(isValidRegex(true)).toBe(false)
36
+ })
37
+
38
+ it('returns true for an empty string (valid regex)', () => {
39
+ expect(isValidRegex('')).toBe(true) // equivalent to: new RegExp('')
40
+ })
41
+ })
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import {
4
+ sanitizeObject,
5
+ sanitizeUndefinedFields,
6
+ sanitizeObjectAllowProps,
7
+ sanitizeObjectDisallowProps,
8
+ } from '../../src/core/sanitize-objects.js'
9
+
10
+ describe('sanitizeObject', () => {
11
+ it('filters object based on provided filter function', () => {
12
+ const result = sanitizeObject({ a: 1, b: 2, c: 3 }, ([key]) => key !== 'b')
13
+ expect(result).toEqual({ a: 1, c: 3 })
14
+ })
15
+
16
+ it('returns empty object when no entries pass the filter', () => {
17
+ const result = sanitizeObject({ a: 1 }, () => false)
18
+ expect(result).toEqual({})
19
+ })
20
+
21
+ it('returns full object when all entries pass the filter', () => {
22
+ const input = { a: 1, b: 2 }
23
+ const result = sanitizeObject(input, () => true)
24
+ expect(result).toEqual(input)
25
+ })
26
+ })
27
+
28
+ describe('sanitizeUndefinedFields', () => {
29
+ it('removes properties with undefined values', () => {
30
+ const result = sanitizeUndefinedFields({ a: 1, b: undefined, c: null })
31
+ expect(result).toEqual({ a: 1, c: null })
32
+ })
33
+
34
+ it('returns empty object if all values are undefined', () => {
35
+ const result = sanitizeUndefinedFields({ a: undefined, b: undefined })
36
+ expect(result).toEqual({})
37
+ })
38
+
39
+ it('does not remove null or falsy (but defined) values', () => {
40
+ const result = sanitizeUndefinedFields({ a: null, b: 0, c: false, d: '' })
41
+ expect(result).toEqual({ a: null, b: 0, c: false, d: '' })
42
+ })
43
+ })
44
+
45
+ describe('sanitizeObjectAllowProps', () => {
46
+ it('keeps only allowed fields', () => {
47
+ const result = sanitizeObjectAllowProps({ a: 1, b: 2, c: 3 }, ['a', 'c'])
48
+ expect(result).toEqual({ a: 1, c: 3 })
49
+ })
50
+
51
+ it('returns empty object if allowedFields is empty', () => {
52
+ const result = sanitizeObjectAllowProps({ a: 1, b: 2 }, [])
53
+ expect(result).toEqual({})
54
+ })
55
+
56
+ it('ignores fields not present in the object', () => {
57
+ const result = sanitizeObjectAllowProps({ a: 1 }, ['a', 'b', 'c'])
58
+ expect(result).toEqual({ a: 1 })
59
+ })
60
+ })
61
+
62
+ describe('sanitizeObjectDisallowProps', () => {
63
+ it('removes disallowed fields from object', () => {
64
+ const result = sanitizeObjectDisallowProps({ a: 1, b: 2, c: 3 }, ['b'])
65
+ expect(result).toEqual({ a: 1, c: 3 })
66
+ })
67
+
68
+ it('returns full object if disallowedFields is empty', () => {
69
+ const input = { a: 1, b: 2 }
70
+ const result = sanitizeObjectDisallowProps(input, [])
71
+ expect(result).toEqual(input)
72
+ })
73
+
74
+ it('removes all properties if all are disallowed', () => {
75
+ const result = sanitizeObjectDisallowProps({ a: 1, b: 2 }, ['a', 'b'])
76
+ expect(result).toEqual({})
77
+ })
78
+ })
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import {
4
+ getSalt,
5
+ encrypt,
6
+ getSaltHex,
7
+ isPasswordMatch,
8
+ getEncryptedBuffer,
9
+ } from '../../src/crypto/crypto.js'
10
+
11
+ const examplePassword = 'S3cretP@ssword'
12
+ const examplePrivateKey = 'my-private-key'
13
+
14
+ describe('getSalt', () => {
15
+ it('returns a Buffer of the correct length', () => {
16
+ const salt = getSalt(16)
17
+ expect(Buffer.isBuffer(salt)).toBe(true)
18
+ expect(salt.length).toBe(16)
19
+ })
20
+ })
21
+
22
+ describe('getSaltHex', () => {
23
+ it('returns a hex string of expected length', () => {
24
+ const saltHex = getSaltHex(16)
25
+ expect(typeof saltHex).toBe('string')
26
+ expect(saltHex.length).toBe(32) // 16 bytes → 32 hex chars
27
+ expect(/^[0-9a-f]+$/i.test(saltHex)).toBe(true)
28
+ })
29
+ })
30
+
31
+ describe('getEncryptedBuffer', () => {
32
+ it('returns a Buffer of the correct derived key length', async () => {
33
+ const salt = 'abc123'
34
+ const buffer = await getEncryptedBuffer({
35
+ expression: examplePassword,
36
+ salt,
37
+ length: 64,
38
+ })
39
+ expect(Buffer.isBuffer(buffer)).toBe(true)
40
+ expect(buffer.length).toBe(64)
41
+ })
42
+ })
43
+
44
+ describe('encrypt', () => {
45
+ it('returns a hex string from encrypted input', async () => {
46
+ const salt = 'abc123'
47
+ const encrypted = await encrypt({
48
+ expression: examplePassword,
49
+ salt,
50
+ passwordPrivateKey: examplePrivateKey,
51
+ })
52
+ expect(typeof encrypted).toBe('string')
53
+ expect(encrypted.length).toBeGreaterThan(0)
54
+ expect(/^[0-9a-f]+$/i.test(encrypted)).toBe(true)
55
+ })
56
+
57
+ it('produces different output when private key changes', async () => {
58
+ const salt = 'abc123'
59
+
60
+ const encrypted1 = await encrypt({
61
+ expression: examplePassword,
62
+ salt,
63
+ passwordPrivateKey: 'A',
64
+ })
65
+
66
+ const encrypted2 = await encrypt({
67
+ expression: examplePassword,
68
+ salt,
69
+ passwordPrivateKey: 'B',
70
+ })
71
+
72
+ expect(encrypted1).not.toEqual(encrypted2)
73
+ })
74
+ })
75
+
76
+ describe('isPasswordMatch', () => {
77
+ it('returns true when password matches encrypted value', async () => {
78
+ const salt = getSaltHex(16)
79
+ const encrypted = await encrypt({
80
+ expression: examplePassword,
81
+ salt,
82
+ passwordPrivateKey: examplePrivateKey,
83
+ })
84
+
85
+ const result = await isPasswordMatch({
86
+ salt,
87
+ password: examplePassword,
88
+ encryptedPassword: encrypted,
89
+ passwordPrivateKey: examplePrivateKey,
90
+ })
91
+
92
+ expect(result).toBe(true)
93
+ })
94
+
95
+ it('returns false when password does not match', async () => {
96
+ const salt = getSaltHex(16)
97
+ const encrypted = await encrypt({
98
+ expression: examplePassword,
99
+ salt,
100
+ passwordPrivateKey: examplePrivateKey,
101
+ })
102
+
103
+ const result = await isPasswordMatch({
104
+ salt,
105
+ password: 'wrong-password',
106
+ encryptedPassword: encrypted,
107
+ passwordPrivateKey: examplePrivateKey,
108
+ })
109
+
110
+ expect(result).toBe(false)
111
+ })
112
+
113
+ it('returns false when private key is incorrect', async () => {
114
+ const salt = getSaltHex(16)
115
+ const encrypted = await encrypt({
116
+ expression: examplePassword,
117
+ salt,
118
+ passwordPrivateKey: 'correct-key',
119
+ })
120
+
121
+ const result = await isPasswordMatch({
122
+ salt,
123
+ password: examplePassword,
124
+ encryptedPassword: encrypted,
125
+ passwordPrivateKey: 'wrong-key',
126
+ })
127
+
128
+ expect(result).toBe(false)
129
+ })
130
+ })
@@ -0,0 +1,73 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ import { encryptPassword } from '../../src/crypto/encryption.js'
5
+
6
+ describe('encryptPassword', () => {
7
+ const password = 'MyS3cretP@ss'
8
+ const privateKey = 'abc123'
9
+ const saltLength = 16
10
+
11
+ it('returns salt and password in expected format', async () => {
12
+ const result = await encryptPassword(
13
+ { password },
14
+ { saltLength, passwordPrivateKey: privateKey },
15
+ )
16
+
17
+ expect(typeof result).toBe('object')
18
+ expect(typeof result.salt).toBe('string')
19
+ expect(typeof result.password).toBe('string')
20
+
21
+ expect(result.salt.length).toBe(saltLength * 2) // because it's hex
22
+ expect(result.salt).toMatch(/^[0-9a-f]+$/i)
23
+ expect(result.password).toMatch(/^[0-9a-f]+$/i)
24
+ })
25
+
26
+ it('produces different output for the same password (due to different salts)', async () => {
27
+ const result1 = await encryptPassword(
28
+ { password },
29
+ { saltLength, passwordPrivateKey: privateKey },
30
+ )
31
+
32
+ const result2 = await encryptPassword(
33
+ { password },
34
+ { saltLength, passwordPrivateKey: privateKey },
35
+ )
36
+
37
+ expect(result1.password).not.toEqual(result2.password)
38
+ expect(result1.salt).not.toEqual(result2.salt)
39
+ })
40
+
41
+ it('produces different output when passwordPrivateKey is changed', async () => {
42
+ const result1 = await encryptPassword(
43
+ { password },
44
+ { saltLength, passwordPrivateKey: 'key1' },
45
+ )
46
+
47
+ const result2 = await encryptPassword(
48
+ { password },
49
+ { saltLength, passwordPrivateKey: 'key2' },
50
+ )
51
+
52
+ expect(result1.password).not.toEqual(result2.password)
53
+ })
54
+
55
+ it('works without passwordPrivateKey', async () => {
56
+ const result = await encryptPassword({ password }, { saltLength })
57
+
58
+ expect(result).toHaveProperty('salt')
59
+ expect(result).toHaveProperty('password')
60
+ })
61
+
62
+ it('throws if password is missing', async () => {
63
+ await expect(() =>
64
+ encryptPassword({}, { saltLength, passwordPrivateKey: privateKey }),
65
+ ).rejects.toThrow()
66
+ })
67
+
68
+ it('throws if saltLength is missing', async () => {
69
+ await expect(() =>
70
+ encryptPassword({ password }, { passwordPrivateKey: privateKey }),
71
+ ).rejects.toThrow()
72
+ })
73
+ })