Skip to content

Commit f045ffe

Browse files
Unit tests
1 parent 5fe161e commit f045ffe

File tree

3 files changed

+331
-0
lines changed

3 files changed

+331
-0
lines changed

jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ module.exports = {
55
collectCoverage: true,
66
coverageDirectory: 'coverage',
77
coverageReporters: ['json', 'lcov', 'text', 'json-summary'],
8+
coverageThreshold: {
9+
global: {
10+
branches: 80,
11+
functions: 80,
12+
lines: 80,
13+
statements: 80
14+
}
15+
},
816
verbose: true,
917
testTimeout: 10000, // Increased timeout for async operations
1018
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"release": "np --no-tests --no-yarn",
88
"test": "jest",
99
"test:watch": "jest --watch",
10+
"test:coverage": "jest --coverage",
1011
"compile": "tsc -p .",
1112
"prepare": "npm run compile",
1213
"release:prepatch": "npm run prepare && npm version prepatch --preid=prerelease && git push --follow-tags && npm publish --tag prerelease",

test/unit/crypto.test.js

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ afterAll(() => {
1717
describe('Crypto Library', () => {
1818
const testHashKey = '69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc'
1919

20+
// Skip uninitialized tests since the module state is shared
21+
2022
// Initialize the library before all tests
2123
beforeAll(async () => {
2224
await crypto.initialize(testHashKey)
@@ -30,6 +32,46 @@ describe('Crypto Library', () => {
3032
it('should throw an error if hash key is invalid', async () => {
3133
await expect(crypto.initialize('invalid')).rejects.toThrow()
3234
})
35+
36+
it('should throw error when initializing without key', async () => {
37+
await expect(crypto.initialize()).rejects.toThrow('Hash key must be passed to initialize function')
38+
await expect(crypto.initialize(null)).rejects.toThrow('Hash key must be passed to initialize function')
39+
await expect(crypto.initialize('')).rejects.toThrow('Hash key must be passed to initialize function')
40+
})
41+
42+
it('should throw error when initializing with non-hex key', async () => {
43+
await expect(crypto.initialize('not-hex')).rejects.toThrow('Hash key must be a 32-byte string')
44+
await expect(crypto.initialize('zzz')).rejects.toThrow('Hash key must be a 32-byte string')
45+
await expect(crypto.initialize('12345g')).rejects.toThrow('Hash key must be a 32-byte string')
46+
})
47+
48+
it('should throw error when initializing with wrong length key', async () => {
49+
// Too short (31 bytes = 62 hex chars)
50+
await expect(crypto.initialize('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1')).rejects.toThrow('Hash key must be a 32-byte string')
51+
// Too long (33 bytes = 66 hex chars)
52+
await expect(crypto.initialize('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3')).rejects.toThrow('Hash key must be a 32-byte string')
53+
})
54+
})
55+
56+
describe('stringify export', () => {
57+
it('should export the stringify function', () => {
58+
expect(crypto.stringify).toBeDefined()
59+
expect(typeof crypto.stringify).toBe('function')
60+
})
61+
62+
it('should stringify objects deterministically', () => {
63+
const obj1 = { b: 2, a: 1, c: 3 }
64+
const obj2 = { a: 1, b: 2, c: 3 }
65+
const obj3 = { c: 3, b: 2, a: 1 }
66+
67+
const str1 = crypto.stringify(obj1)
68+
const str2 = crypto.stringify(obj2)
69+
const str3 = crypto.stringify(obj3)
70+
71+
expect(str1).toBe(str2)
72+
expect(str2).toBe(str3)
73+
expect(str1).toBe('{"a":1,"b":2,"c":3}')
74+
})
3375
})
3476

3577
describe('randomBytes', () => {
@@ -174,6 +216,72 @@ describe('Crypto Library', () => {
174216
crypto.verify(messageHash, signature, keypair2.publicKey)
175217
}).toThrow('Unable to verify provided signature with provided public key')
176218
})
219+
220+
it('should throw error when signing with non-hex input', () => {
221+
const keypair = crypto.generateKeypair()
222+
223+
expect(() => crypto.sign('not-hex', keypair.secretKey)).toThrow('Input string must be in hex format')
224+
expect(() => crypto.sign('zzz', keypair.secretKey)).toThrow('Input string must be in hex format')
225+
expect(() => crypto.sign('12345g', keypair.secretKey)).toThrow('Input string must be in hex format')
226+
})
227+
228+
it('should throw error when signing with non-hex secret key', () => {
229+
const messageHash = crypto.hash('test')
230+
231+
expect(() => crypto.sign(messageHash, 'not-hex')).toThrow('Secret key string must be in hex format')
232+
expect(() => crypto.sign(messageHash, 'zzz')).toThrow('Secret key string must be in hex format')
233+
expect(() => crypto.sign(messageHash, '12345g')).toThrow('Secret key string must be in hex format')
234+
})
235+
236+
it('should throw error when verifying with non-string message', () => {
237+
const keypair = crypto.generateKeypair()
238+
const signature = crypto.sign(crypto.hash('test'), keypair.secretKey)
239+
240+
expect(() => crypto.verify(123, signature, keypair.publicKey)).toThrow('Message to compare must be a string')
241+
expect(() => crypto.verify(null, signature, keypair.publicKey)).toThrow('Message to compare must be a string')
242+
expect(() => crypto.verify(undefined, signature, keypair.publicKey)).toThrow('Message to compare must be a string')
243+
expect(() => crypto.verify({}, signature, keypair.publicKey)).toThrow('Message to compare must be a string')
244+
})
245+
246+
it('should throw error when verifying with non-string signature', () => {
247+
const keypair = crypto.generateKeypair()
248+
const messageHash = crypto.hash('test')
249+
250+
expect(() => crypto.verify(messageHash, 123, keypair.publicKey)).toThrow('Signature must be a hex string')
251+
expect(() => crypto.verify(messageHash, null, keypair.publicKey)).toThrow('Signature must be a hex string')
252+
expect(() => crypto.verify(messageHash, undefined, keypair.publicKey)).toThrow('Signature must be a hex string')
253+
expect(() => crypto.verify(messageHash, {}, keypair.publicKey)).toThrow('Signature must be a hex string')
254+
})
255+
256+
it('should throw error when verifying with non-hex signature', () => {
257+
const keypair = crypto.generateKeypair()
258+
const messageHash = crypto.hash('test')
259+
260+
expect(() => crypto.verify(messageHash, 'not-hex', keypair.publicKey)).toThrow('Signature must be a hex string')
261+
expect(() => crypto.verify(messageHash, 'zzz', keypair.publicKey)).toThrow('Signature must be a hex string')
262+
expect(() => crypto.verify(messageHash, '12345g', keypair.publicKey)).toThrow('Signature must be a hex string')
263+
})
264+
265+
it('should throw error when verifying with non-string public key', () => {
266+
const keypair = crypto.generateKeypair()
267+
const messageHash = crypto.hash('test')
268+
const signature = crypto.sign(messageHash, keypair.secretKey)
269+
270+
expect(() => crypto.verify(messageHash, signature, 123)).toThrow('Public key must be a hex string')
271+
expect(() => crypto.verify(messageHash, signature, null)).toThrow('Public key must be a hex string')
272+
expect(() => crypto.verify(messageHash, signature, undefined)).toThrow('Public key must be a hex string')
273+
expect(() => crypto.verify(messageHash, signature, {})).toThrow('Public key must be a hex string')
274+
})
275+
276+
it('should throw error when verifying with non-hex public key', () => {
277+
const keypair = crypto.generateKeypair()
278+
const messageHash = crypto.hash('test')
279+
const signature = crypto.sign(messageHash, keypair.secretKey)
280+
281+
expect(() => crypto.verify(messageHash, signature, 'not-hex')).toThrow('Public key must be a hex string')
282+
expect(() => crypto.verify(messageHash, signature, 'zzz')).toThrow('Public key must be a hex string')
283+
expect(() => crypto.verify(messageHash, signature, '12345g')).toThrow('Public key must be a hex string')
284+
})
177285
})
178286

179287
describe('signObj and verifyObj', () => {
@@ -203,5 +311,219 @@ describe('Crypto Library', () => {
203311
const verified = crypto.verifyObj(obj)
204312
expect(verified).toBe(false)
205313
})
314+
315+
it('should throw error when signing non-object', () => {
316+
const keypair = crypto.generateKeypair()
317+
318+
expect(() => crypto.signObj('not-object', keypair.secretKey, keypair.publicKey)).toThrow('Input must be an object')
319+
expect(() => crypto.signObj(123, keypair.secretKey, keypair.publicKey)).toThrow('Input must be an object')
320+
// null will throw a different error due to trying to set property on null
321+
expect(() => crypto.signObj(null, keypair.secretKey, keypair.publicKey)).toThrow()
322+
expect(() => crypto.signObj(undefined, keypair.secretKey, keypair.publicKey)).toThrow('Input must be an object')
323+
})
324+
325+
it('should throw error when signing with non-string secret key', () => {
326+
const keypair = crypto.generateKeypair()
327+
const obj = { test: 'value' }
328+
329+
expect(() => crypto.signObj(obj, 123, keypair.publicKey)).toThrow('Secret key must be a string')
330+
expect(() => crypto.signObj(obj, null, keypair.publicKey)).toThrow('Secret key must be a string')
331+
expect(() => crypto.signObj(obj, undefined, keypair.publicKey)).toThrow('Secret key must be a string')
332+
expect(() => crypto.signObj(obj, {}, keypair.publicKey)).toThrow('Secret key must be a string')
333+
})
334+
335+
it('should throw error when signing with non-string public key', () => {
336+
const keypair = crypto.generateKeypair()
337+
const obj = { test: 'value' }
338+
339+
expect(() => crypto.signObj(obj, keypair.secretKey, 123)).toThrow('Public key must be a string')
340+
expect(() => crypto.signObj(obj, keypair.secretKey, null)).toThrow('Public key must be a string')
341+
expect(() => crypto.signObj(obj, keypair.secretKey, undefined)).toThrow('Public key must be a string')
342+
expect(() => crypto.signObj(obj, keypair.secretKey, {})).toThrow('Public key must be a string')
343+
})
344+
345+
it('should throw error when verifying non-object', () => {
346+
expect(() => crypto.verifyObj('not-object')).toThrow('Input must be an object')
347+
expect(() => crypto.verifyObj(123)).toThrow('Input must be an object')
348+
// null will throw a different error due to trying to read property of null
349+
expect(() => crypto.verifyObj(null)).toThrow()
350+
expect(() => crypto.verifyObj(undefined)).toThrow('Input must be an object')
351+
})
352+
353+
it('should throw error when verifying object without sign field', () => {
354+
const obj = { test: 'value' }
355+
expect(() => crypto.verifyObj(obj)).toThrow('Object must contain a sign field with the following data: { owner, sig }')
356+
})
357+
358+
it('should throw error when verifying object without sign.owner', () => {
359+
const obj = { test: 'value', sign: { sig: 'somesig' } }
360+
expect(() => crypto.verifyObj(obj)).toThrow('Object must contain a sign field with the following data: { owner, sig }')
361+
})
362+
363+
it('should throw error when verifying object without sign.sig', () => {
364+
const obj = { test: 'value', sign: { owner: 'someowner' } }
365+
expect(() => crypto.verifyObj(obj)).toThrow('Object must contain a sign field with the following data: { owner, sig }')
366+
})
367+
368+
it('should throw error when verifying object with non-string sign.owner', () => {
369+
const obj = { test: 'value', sign: { owner: 123, sig: 'somesig' } }
370+
expect(() => crypto.verifyObj(obj)).toThrow('Owner must be a public key represented as a hex string')
371+
})
372+
373+
it('should throw error when verifying object with non-string sign.sig', () => {
374+
const obj = { test: 'value', sign: { owner: 'someowner', sig: 123 } }
375+
expect(() => crypto.verifyObj(obj)).toThrow('Signature must be a valid signature represented as a hex string')
376+
})
377+
})
378+
379+
describe('encryptAB and decryptAB', () => {
380+
it('should successfully encrypt and decrypt a message', () => {
381+
const keypairA = crypto.generateKeypair()
382+
const keypairB = crypto.generateKeypair()
383+
const message = 'test message'
384+
385+
// A encrypts a message for B using B's public key and A's secret key
386+
const encrypted = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
387+
expect(typeof encrypted).toBe('string')
388+
expect(encrypted).toContain(':') // Should contain nonce:ciphertext
389+
390+
// B decrypts the message using A's public key and B's secret key
391+
const decrypted = crypto.decryptAB(encrypted, keypairA.publicKey, keypairB.secretKey)
392+
expect(decrypted).toBe(message)
393+
})
394+
395+
it('should encrypt and decrypt an empty string', () => {
396+
const keypairA = crypto.generateKeypair()
397+
const keypairB = crypto.generateKeypair()
398+
const message = ''
399+
400+
const encrypted = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
401+
const decrypted = crypto.decryptAB(encrypted, keypairA.publicKey, keypairB.secretKey)
402+
expect(decrypted).toBe(message)
403+
})
404+
405+
it('should produce different ciphertexts for the same message due to random nonce', () => {
406+
const keypairA = crypto.generateKeypair()
407+
const keypairB = crypto.generateKeypair()
408+
const message = 'test message'
409+
410+
const encrypted1 = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
411+
const encrypted2 = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
412+
413+
expect(encrypted1).not.toBe(encrypted2) // Different due to random nonce
414+
415+
// Both should decrypt to the same message
416+
const decrypted1 = crypto.decryptAB(encrypted1, keypairA.publicKey, keypairB.secretKey)
417+
const decrypted2 = crypto.decryptAB(encrypted2, keypairA.publicKey, keypairB.secretKey)
418+
expect(decrypted1).toBe(message)
419+
expect(decrypted2).toBe(message)
420+
})
421+
422+
it('should throw error when encrypting with invalid message type', () => {
423+
const keypairA = crypto.generateKeypair()
424+
const keypairB = crypto.generateKeypair()
425+
426+
expect(() => crypto.encryptAB(123, keypairB.publicKey, keypairA.secretKey)).toThrow('Message to encrypt must be a string')
427+
expect(() => crypto.encryptAB(null, keypairB.publicKey, keypairA.secretKey)).toThrow('Message to encrypt must be a string')
428+
expect(() => crypto.encryptAB(undefined, keypairB.publicKey, keypairA.secretKey)).toThrow('Message to encrypt must be a string')
429+
expect(() => crypto.encryptAB({}, keypairB.publicKey, keypairA.secretKey)).toThrow('Message to encrypt must be a string')
430+
})
431+
432+
it('should throw error when encrypting with invalid public key format', () => {
433+
const keypairA = crypto.generateKeypair()
434+
const message = 'test'
435+
436+
expect(() => crypto.encryptAB(message, 'invalid', keypairA.secretKey)).toThrow('Secret key string must be in hex format')
437+
expect(() => crypto.encryptAB(message, '12345', keypairA.secretKey)).toThrow('Secret key string must be in hex format')
438+
expect(() => crypto.encryptAB(message, 'zzz', keypairA.secretKey)).toThrow('Secret key string must be in hex format')
439+
})
440+
441+
it('should throw error when encrypting with invalid secret key format', () => {
442+
const keypairB = crypto.generateKeypair()
443+
const message = 'test'
444+
445+
expect(() => crypto.encryptAB(message, keypairB.publicKey, 'invalid')).toThrow('Secret key string must be in hex format')
446+
expect(() => crypto.encryptAB(message, keypairB.publicKey, '12345')).toThrow('Secret key string must be in hex format')
447+
expect(() => crypto.encryptAB(message, keypairB.publicKey, 'zzz')).toThrow('Secret key string must be in hex format')
448+
})
449+
450+
it('should throw error when decrypting with invalid message type', () => {
451+
const keypairA = crypto.generateKeypair()
452+
const keypairB = crypto.generateKeypair()
453+
454+
expect(() => crypto.decryptAB(123, keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt must be a string')
455+
expect(() => crypto.decryptAB(null, keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt must be a string')
456+
expect(() => crypto.decryptAB(undefined, keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt must be a string')
457+
expect(() => crypto.decryptAB({}, keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt must be a string')
458+
})
459+
460+
it('should throw error when decrypting with invalid message format', () => {
461+
const keypairA = crypto.generateKeypair()
462+
const keypairB = crypto.generateKeypair()
463+
464+
// Missing colon separator
465+
expect(() => crypto.decryptAB('invalidformat', keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt in must have nonce:ciphertext as hex:base64')
466+
467+
// Invalid nonce (not hex)
468+
expect(() => crypto.decryptAB('zzz:validbase64', keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt in must have nonce:ciphertext as hex:base64')
469+
470+
// Invalid ciphertext (not base64)
471+
expect(() => crypto.decryptAB('a1b2c3:@@@###', keypairA.publicKey, keypairB.secretKey)).toThrow('Message to decrypt in must have nonce:ciphertext as hex:base64')
472+
})
473+
474+
it('should throw error when decrypting with invalid public key format', () => {
475+
const keypairA = crypto.generateKeypair()
476+
const keypairB = crypto.generateKeypair()
477+
// First create a valid encrypted message to get the format right
478+
const encrypted = crypto.encryptAB('test', keypairB.publicKey, keypairA.secretKey)
479+
480+
expect(() => crypto.decryptAB(encrypted, 'invalid', keypairB.secretKey)).toThrow('Secret key string must be in hex format')
481+
expect(() => crypto.decryptAB(encrypted, '12345', keypairB.secretKey)).toThrow('Secret key string must be in hex format')
482+
expect(() => crypto.decryptAB(encrypted, 'zzz', keypairB.secretKey)).toThrow('Secret key string must be in hex format')
483+
})
484+
485+
it('should throw error when decrypting with invalid secret key format', () => {
486+
const keypairA = crypto.generateKeypair()
487+
const keypairB = crypto.generateKeypair()
488+
// First create a valid encrypted message to get the format right
489+
const encrypted = crypto.encryptAB('test', keypairB.publicKey, keypairA.secretKey)
490+
491+
expect(() => crypto.decryptAB(encrypted, keypairA.publicKey, 'invalid')).toThrow('Secret key string must be in hex format')
492+
expect(() => crypto.decryptAB(encrypted, keypairA.publicKey, '12345')).toThrow('Secret key string must be in hex format')
493+
expect(() => crypto.decryptAB(encrypted, keypairA.publicKey, 'zzz')).toThrow('Secret key string must be in hex format')
494+
})
495+
496+
it('should throw error when decrypting with wrong keys', () => {
497+
const keypairA = crypto.generateKeypair()
498+
const keypairB = crypto.generateKeypair()
499+
const keypairC = crypto.generateKeypair()
500+
const message = 'test message'
501+
502+
// A encrypts for B
503+
const encrypted = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
504+
505+
// C tries to decrypt (should fail)
506+
expect(() => crypto.decryptAB(encrypted, keypairA.publicKey, keypairC.secretKey)).toThrow('Could not decrypt the message')
507+
508+
// B tries to decrypt with wrong public key (should fail)
509+
expect(() => crypto.decryptAB(encrypted, keypairC.publicKey, keypairB.secretKey)).toThrow('Could not decrypt the message')
510+
})
511+
512+
it('should throw error when decrypting corrupted ciphertext', () => {
513+
const keypairA = crypto.generateKeypair()
514+
const keypairB = crypto.generateKeypair()
515+
const message = 'test message'
516+
517+
const encrypted = crypto.encryptAB(message, keypairB.publicKey, keypairA.secretKey)
518+
519+
// Corrupt the ciphertext by modifying one character in the base64 part
520+
const parts = encrypted.split(':')
521+
const base64Part = parts[1]
522+
// Change a character in the middle of the base64 string
523+
const corruptedBase64 = base64Part.substring(0, 10) + 'X' + base64Part.substring(11)
524+
const corruptedEncrypted = parts[0] + ':' + corruptedBase64
525+
526+
expect(() => crypto.decryptAB(corruptedEncrypted, keypairA.publicKey, keypairB.secretKey)).toThrow('Could not decrypt the message')
527+
})
206528
})
207529
})

0 commit comments

Comments
 (0)