@learncard/sss-key-manager 0.1.14 β†’ 0.1.15

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.
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Critical Path Tests
3
+ *
4
+ * These tests verify that private keys can NEVER be lost due to:
5
+ * - Share splitting bugs
6
+ * - Recovery method round-trip failures
7
+ * - Partial storage failures
8
+ *
9
+ * Run these tests before any release that touches SSS code.
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+
14
+ import { splitPrivateKey, reconstructFromShares, SSS_THRESHOLD } from './sss';
15
+ import { generateEd25519PrivateKey, encryptWithPassword, decryptWithPassword } from './crypto';
16
+ import {
17
+ shareToRecoveryPhrase,
18
+ recoveryPhraseToShare,
19
+ validateRecoveryPhrase,
20
+ } from './recovery-phrase';
21
+
22
+ describe('Critical: Key must NEVER be lost', () => {
23
+ describe('Share split verification (fuzz test)', () => {
24
+ it('should verify all 6 share combinations reconstruct the key (100 iterations)', async () => {
25
+ for (let i = 0; i < 100; i++) {
26
+ const privateKey = await generateEd25519PrivateKey();
27
+ const shares = await splitPrivateKey(privateKey);
28
+
29
+ // All 6 combinations (C(4,2)) must reconstruct the exact same key
30
+ const fromDeviceAuth = await reconstructFromShares([
31
+ shares.deviceShare,
32
+ shares.authShare,
33
+ ]);
34
+ expect(fromDeviceAuth).toBe(privateKey);
35
+
36
+ const fromDeviceRecovery = await reconstructFromShares([
37
+ shares.deviceShare,
38
+ shares.recoveryShare,
39
+ ]);
40
+ expect(fromDeviceRecovery).toBe(privateKey);
41
+
42
+ const fromDeviceEmail = await reconstructFromShares([
43
+ shares.deviceShare,
44
+ shares.emailShare,
45
+ ]);
46
+ expect(fromDeviceEmail).toBe(privateKey);
47
+
48
+ const fromAuthRecovery = await reconstructFromShares([
49
+ shares.authShare,
50
+ shares.recoveryShare,
51
+ ]);
52
+ expect(fromAuthRecovery).toBe(privateKey);
53
+
54
+ const fromAuthEmail = await reconstructFromShares([
55
+ shares.authShare,
56
+ shares.emailShare,
57
+ ]);
58
+ expect(fromAuthEmail).toBe(privateKey);
59
+
60
+ const fromRecoveryEmail = await reconstructFromShares([
61
+ shares.recoveryShare,
62
+ shares.emailShare,
63
+ ]);
64
+ expect(fromRecoveryEmail).toBe(privateKey);
65
+ }
66
+ });
67
+
68
+ it('should produce unique shares for each split', async () => {
69
+ const privateKey = await generateEd25519PrivateKey();
70
+
71
+ const shares1 = await splitPrivateKey(privateKey);
72
+ const shares2 = await splitPrivateKey(privateKey);
73
+
74
+ // Different splits produce different shares (due to randomness in SSS)
75
+ expect(shares1.deviceShare).not.toBe(shares2.deviceShare);
76
+ expect(shares1.authShare).not.toBe(shares2.authShare);
77
+ expect(shares1.recoveryShare).not.toBe(shares2.recoveryShare);
78
+ expect(shares1.emailShare).not.toBe(shares2.emailShare);
79
+
80
+ // But both should still reconstruct to the same key
81
+ const reconstructed1 = await reconstructFromShares([
82
+ shares1.deviceShare,
83
+ shares1.authShare,
84
+ ]);
85
+ const reconstructed2 = await reconstructFromShares([
86
+ shares2.deviceShare,
87
+ shares2.authShare,
88
+ ]);
89
+
90
+ expect(reconstructed1).toBe(privateKey);
91
+ expect(reconstructed2).toBe(privateKey);
92
+ });
93
+
94
+ it('should handle edge case private keys', async () => {
95
+ const edgeCases = [
96
+ '0'.repeat(64), // All zeros
97
+ 'f'.repeat(64), // All ones
98
+ '0f'.repeat(32), // Alternating
99
+ 'abcdef0123456789'.repeat(4), // Pattern
100
+ ];
101
+
102
+ for (const privateKey of edgeCases) {
103
+ const shares = await splitPrivateKey(privateKey);
104
+
105
+ const reconstructed = await reconstructFromShares([
106
+ shares.deviceShare,
107
+ shares.authShare,
108
+ ]);
109
+
110
+ expect(reconstructed).toBe(privateKey);
111
+ }
112
+ });
113
+
114
+ it('should reject reconstruction with fewer than threshold shares', async () => {
115
+ const privateKey = await generateEd25519PrivateKey();
116
+ const shares = await splitPrivateKey(privateKey);
117
+
118
+ await expect(reconstructFromShares([shares.deviceShare])).rejects.toThrow(
119
+ `Need at least ${SSS_THRESHOLD} shares`
120
+ );
121
+ });
122
+ });
123
+
124
+ describe('Password encryption round-trip (used by backup files)', () => {
125
+ it('should preserve exact private key through password encryption/decryption', async () => {
126
+ const privateKey = await generateEd25519PrivateKey();
127
+ const password = 'test-password-123!@#';
128
+
129
+ const shares = await splitPrivateKey(privateKey);
130
+ const encrypted = await encryptWithPassword(shares.recoveryShare, password);
131
+
132
+ const decrypted = await decryptWithPassword(
133
+ encrypted.ciphertext,
134
+ encrypted.iv,
135
+ encrypted.salt,
136
+ password,
137
+ encrypted.kdfParams
138
+ );
139
+
140
+ expect(decrypted).toBe(shares.recoveryShare);
141
+
142
+ const reconstructed = await reconstructFromShares([decrypted, shares.authShare]);
143
+
144
+ expect(reconstructed).toBe(privateKey);
145
+ });
146
+
147
+ it('should fail with wrong password', async () => {
148
+ const privateKey = await generateEd25519PrivateKey();
149
+ const shares = await splitPrivateKey(privateKey);
150
+ const encrypted = await encryptWithPassword(shares.recoveryShare, 'correct-password');
151
+
152
+ await expect(
153
+ decryptWithPassword(
154
+ encrypted.ciphertext,
155
+ encrypted.iv,
156
+ encrypted.salt,
157
+ 'wrong-password',
158
+ encrypted.kdfParams
159
+ )
160
+ ).rejects.toThrow();
161
+ });
162
+
163
+ it('should handle various password strengths', async () => {
164
+ const passwords = [
165
+ 'short',
166
+ 'medium-length-password',
167
+ 'very-long-password-with-special-characters-!@#$%^&*()',
168
+ 'ζ—₯本θͺžγƒ‘γ‚Ήγƒ―ード', // Unicode
169
+ 'πŸ”πŸ”‘πŸ”’', // Emoji
170
+ ];
171
+
172
+ for (const password of passwords) {
173
+ const privateKey = await generateEd25519PrivateKey();
174
+ const shares = await splitPrivateKey(privateKey);
175
+ const encrypted = await encryptWithPassword(shares.recoveryShare, password);
176
+
177
+ const decrypted = await decryptWithPassword(
178
+ encrypted.ciphertext,
179
+ encrypted.iv,
180
+ encrypted.salt,
181
+ password,
182
+ encrypted.kdfParams
183
+ );
184
+
185
+ const reconstructed = await reconstructFromShares([decrypted, shares.authShare]);
186
+
187
+ expect(reconstructed).toBe(privateKey);
188
+ }
189
+ }, 15_000);
190
+ });
191
+
192
+ describe('Recovery phrase round-trip', () => {
193
+ it('should preserve exact private key through phrase encoding/decoding', async () => {
194
+ const privateKey = await generateEd25519PrivateKey();
195
+ const shares = await splitPrivateKey(privateKey);
196
+
197
+ const phrase = await shareToRecoveryPhrase(shares.recoveryShare);
198
+ const isValid = await validateRecoveryPhrase(phrase);
199
+ expect(isValid).toBe(true);
200
+
201
+ const recoveredShare = await recoveryPhraseToShare(phrase);
202
+ expect(recoveredShare).toBe(shares.recoveryShare);
203
+
204
+ const reconstructed = await reconstructFromShares([recoveredShare, shares.authShare]);
205
+
206
+ expect(reconstructed).toBe(privateKey);
207
+ });
208
+
209
+ it('should handle 100 random private keys', async () => {
210
+ for (let i = 0; i < 100; i++) {
211
+ const privateKey = await generateEd25519PrivateKey();
212
+ const shares = await splitPrivateKey(privateKey);
213
+
214
+ const phrase = await shareToRecoveryPhrase(shares.recoveryShare);
215
+ const recoveredShare = await recoveryPhraseToShare(phrase);
216
+
217
+ const reconstructed = await reconstructFromShares([
218
+ recoveredShare,
219
+ shares.authShare,
220
+ ]);
221
+
222
+ expect(reconstructed).toBe(privateKey);
223
+ }
224
+ });
225
+
226
+ it('should produce consistent word count for SSS shares', async () => {
227
+ // SSS shares include an index byte (33 bytes total)
228
+ // This produces 25 words instead of standard BIP39 24 words
229
+ const privateKey = await generateEd25519PrivateKey();
230
+ const shares = await splitPrivateKey(privateKey);
231
+
232
+ const phrase = await shareToRecoveryPhrase(shares.recoveryShare);
233
+ const wordCount = phrase.split(' ').length;
234
+
235
+ // SSS shares are 33 bytes, which produces 25 words
236
+ expect(wordCount).toBeGreaterThanOrEqual(24);
237
+ expect(wordCount).toBeLessThanOrEqual(27);
238
+ });
239
+
240
+ it('should handle phrase with extra whitespace', async () => {
241
+ const privateKey = await generateEd25519PrivateKey();
242
+ const shares = await splitPrivateKey(privateKey);
243
+
244
+ const phrase = await shareToRecoveryPhrase(shares.recoveryShare);
245
+ const paddedPhrase = ` ${phrase.split(' ').join(' ')} `;
246
+
247
+ const recoveredShare = await recoveryPhraseToShare(paddedPhrase);
248
+ expect(recoveredShare).toBe(shares.recoveryShare);
249
+ });
250
+
251
+ it('should handle phrase with mixed case', async () => {
252
+ const privateKey = await generateEd25519PrivateKey();
253
+ const shares = await splitPrivateKey(privateKey);
254
+
255
+ const phrase = await shareToRecoveryPhrase(shares.recoveryShare);
256
+ const upperPhrase = phrase.toUpperCase();
257
+
258
+ const recoveredShare = await recoveryPhraseToShare(upperPhrase);
259
+ expect(recoveredShare).toBe(shares.recoveryShare);
260
+ });
261
+ });
262
+
263
+ describe('Backup file round-trip', () => {
264
+ it('should preserve exact private key through backup file flow', async () => {
265
+ const privateKey = await generateEd25519PrivateKey();
266
+ const backupPassword = 'backup-file-password-123';
267
+
268
+ // Simulate creating a backup
269
+ const shares = await splitPrivateKey(privateKey);
270
+ const encrypted = await encryptWithPassword(shares.recoveryShare, backupPassword);
271
+
272
+ const backupFile = {
273
+ version: 1,
274
+ createdAt: new Date().toISOString(),
275
+ primaryDid: 'did:key:test123',
276
+ encryptedShare: {
277
+ ciphertext: encrypted.ciphertext,
278
+ iv: encrypted.iv,
279
+ salt: encrypted.salt,
280
+ kdfParams: encrypted.kdfParams,
281
+ },
282
+ };
283
+
284
+ // Simulate saving and loading (JSON round-trip)
285
+ const backupJson = JSON.stringify(backupFile);
286
+ const restoredBackup = JSON.parse(backupJson);
287
+
288
+ // Restore from backup
289
+ const recoveredShare = await decryptWithPassword(
290
+ restoredBackup.encryptedShare.ciphertext,
291
+ restoredBackup.encryptedShare.iv,
292
+ restoredBackup.encryptedShare.salt,
293
+ backupPassword,
294
+ restoredBackup.encryptedShare.kdfParams
295
+ );
296
+
297
+ const reconstructed = await reconstructFromShares([recoveredShare, shares.authShare]);
298
+
299
+ expect(reconstructed).toBe(privateKey);
300
+ });
301
+ });
302
+
303
+ describe('Cross-share compatibility', () => {
304
+ it('should NOT reconstruct key from shares of different splits', async () => {
305
+ const privateKey = await generateEd25519PrivateKey();
306
+
307
+ // Two different splits
308
+ const shares1 = await splitPrivateKey(privateKey);
309
+ const shares2 = await splitPrivateKey(privateKey);
310
+
311
+ // Mixing shares from different splits should produce WRONG key
312
+ const wrongKey = await reconstructFromShares([
313
+ shares1.deviceShare, // From split 1
314
+ shares2.authShare, // From split 2
315
+ ]);
316
+
317
+ // The reconstructed key should NOT match the original
318
+ // (This is the bug we fixed - stale device shares)
319
+ expect(wrongKey).not.toBe(privateKey);
320
+ });
321
+
322
+ it('should detect stale device shares via DID mismatch', async () => {
323
+ const privateKey = await generateEd25519PrivateKey();
324
+
325
+ // Original split
326
+ const originalShares = await splitPrivateKey(privateKey);
327
+
328
+ // After recovery, new split is created
329
+ const newShares = await splitPrivateKey(privateKey);
330
+
331
+ // Reconstruct with stale device + new auth
332
+ const wrongKey = await reconstructFromShares([
333
+ originalShares.deviceShare, // Stale
334
+ newShares.authShare, // New
335
+ ]);
336
+
337
+ // Keys don't match - this is the scenario DID verification catches
338
+ expect(wrongKey).not.toBe(privateKey);
339
+ });
340
+ });
341
+ });
342
+
343
+ describe('Critical: Share integrity', () => {
344
+ it('shares should be valid hex strings', async () => {
345
+ const privateKey = await generateEd25519PrivateKey();
346
+ const shares = await splitPrivateKey(privateKey);
347
+
348
+ const hexRegex = /^[0-9a-f]+$/i;
349
+
350
+ expect(hexRegex.test(shares.deviceShare)).toBe(true);
351
+ expect(hexRegex.test(shares.authShare)).toBe(true);
352
+ expect(hexRegex.test(shares.recoveryShare)).toBe(true);
353
+ expect(hexRegex.test(shares.emailShare)).toBe(true);
354
+ });
355
+
356
+ it('shares should be consistent length', async () => {
357
+ const privateKey = await generateEd25519PrivateKey();
358
+ const shares = await splitPrivateKey(privateKey);
359
+
360
+ // All shares should be the same length
361
+ expect(shares.deviceShare.length).toBe(shares.authShare.length);
362
+ expect(shares.authShare.length).toBe(shares.recoveryShare.length);
363
+ expect(shares.recoveryShare.length).toBe(shares.emailShare.length);
364
+
365
+ // SSS adds an index byte, so shares are 33 bytes (66 hex chars)
366
+ expect(shares.deviceShare.length).toBe(66);
367
+ });
368
+
369
+ it('shares should be unique within a split', async () => {
370
+ const privateKey = await generateEd25519PrivateKey();
371
+ const shares = await splitPrivateKey(privateKey);
372
+
373
+ expect(shares.deviceShare).not.toBe(shares.authShare);
374
+ expect(shares.authShare).not.toBe(shares.recoveryShare);
375
+ expect(shares.deviceShare).not.toBe(shares.recoveryShare);
376
+ expect(shares.deviceShare).not.toBe(shares.emailShare);
377
+ expect(shares.authShare).not.toBe(shares.emailShare);
378
+ expect(shares.recoveryShare).not.toBe(shares.emailShare);
379
+ });
380
+ });
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ encryptWithPassword,
4
+ decryptWithPassword,
5
+ deriveKeyFromPassword,
6
+ generateEd25519PrivateKey,
7
+ DEFAULT_KDF_PARAMS,
8
+ hexToBytes,
9
+ bytesToHex,
10
+ } from './crypto';
11
+
12
+ describe('Password-based encryption', () => {
13
+ const testPassword = 'testPassword123!@#';
14
+ const testData = 'sensitive-recovery-share-data-hex-string-1234567890abcdef';
15
+
16
+ describe('encryptWithPassword', () => {
17
+ it('should encrypt data and return ciphertext, iv, salt, and kdfParams', async () => {
18
+ const result = await encryptWithPassword(testData, testPassword);
19
+
20
+ expect(result.ciphertext).toBeDefined();
21
+ expect(result.iv).toBeDefined();
22
+ expect(result.salt).toBeDefined();
23
+ expect(result.kdfParams).toBeDefined();
24
+
25
+ expect(result.ciphertext.length).toBeGreaterThan(0);
26
+ expect(result.iv.length).toBeGreaterThan(0);
27
+ expect(result.salt.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it('should produce different ciphertexts for same data with same password (due to random salt/iv)', async () => {
31
+ const result1 = await encryptWithPassword(testData, testPassword);
32
+ const result2 = await encryptWithPassword(testData, testPassword);
33
+
34
+ expect(result1.ciphertext).not.toBe(result2.ciphertext);
35
+ expect(result1.salt).not.toBe(result2.salt);
36
+ expect(result1.iv).not.toBe(result2.iv);
37
+ });
38
+
39
+ it('should use Argon2id algorithm by default', async () => {
40
+ const result = await encryptWithPassword(testData, testPassword);
41
+
42
+ expect(result.kdfParams.algorithm).toBe('argon2id');
43
+ });
44
+ });
45
+
46
+ describe('decryptWithPassword', () => {
47
+ it('should decrypt data encrypted with encryptWithPassword', async () => {
48
+ const encrypted = await encryptWithPassword(testData, testPassword);
49
+
50
+ const decrypted = await decryptWithPassword(
51
+ encrypted.ciphertext,
52
+ encrypted.iv,
53
+ encrypted.salt,
54
+ testPassword,
55
+ encrypted.kdfParams
56
+ );
57
+
58
+ expect(decrypted).toBe(testData);
59
+ });
60
+
61
+ it('should fail with wrong password', async () => {
62
+ const encrypted = await encryptWithPassword(testData, testPassword);
63
+
64
+ await expect(
65
+ decryptWithPassword(
66
+ encrypted.ciphertext,
67
+ encrypted.iv,
68
+ encrypted.salt,
69
+ 'wrongPassword',
70
+ encrypted.kdfParams
71
+ )
72
+ ).rejects.toThrow();
73
+ });
74
+
75
+ it('should fail with corrupted ciphertext', async () => {
76
+ const encrypted = await encryptWithPassword(testData, testPassword);
77
+
78
+ const corruptedCiphertext = 'corrupted' + encrypted.ciphertext.slice(9);
79
+
80
+ await expect(
81
+ decryptWithPassword(
82
+ corruptedCiphertext,
83
+ encrypted.iv,
84
+ encrypted.salt,
85
+ testPassword,
86
+ encrypted.kdfParams
87
+ )
88
+ ).rejects.toThrow();
89
+ });
90
+
91
+ it('should handle empty string data', async () => {
92
+ const emptyData = '';
93
+ const encrypted = await encryptWithPassword(emptyData, testPassword);
94
+
95
+ const decrypted = await decryptWithPassword(
96
+ encrypted.ciphertext,
97
+ encrypted.iv,
98
+ encrypted.salt,
99
+ testPassword,
100
+ encrypted.kdfParams
101
+ );
102
+
103
+ expect(decrypted).toBe(emptyData);
104
+ });
105
+
106
+ it('should handle long data strings', async () => {
107
+ const longData = 'x'.repeat(10000);
108
+ const encrypted = await encryptWithPassword(longData, testPassword);
109
+
110
+ const decrypted = await decryptWithPassword(
111
+ encrypted.ciphertext,
112
+ encrypted.iv,
113
+ encrypted.salt,
114
+ testPassword,
115
+ encrypted.kdfParams
116
+ );
117
+
118
+ expect(decrypted).toBe(longData);
119
+ });
120
+
121
+ it('should handle unicode data', async () => {
122
+ const unicodeData = 'πŸ” Recovery Share 密码 مفΨͺΨ§Ψ­';
123
+ const encrypted = await encryptWithPassword(unicodeData, testPassword);
124
+
125
+ const decrypted = await decryptWithPassword(
126
+ encrypted.ciphertext,
127
+ encrypted.iv,
128
+ encrypted.salt,
129
+ testPassword,
130
+ encrypted.kdfParams
131
+ );
132
+
133
+ expect(decrypted).toBe(unicodeData);
134
+ });
135
+
136
+ it('should handle unicode passwords', async () => {
137
+ const unicodePassword = 'パスワード123πŸ”‘';
138
+ const encrypted = await encryptWithPassword(testData, unicodePassword);
139
+
140
+ const decrypted = await decryptWithPassword(
141
+ encrypted.ciphertext,
142
+ encrypted.iv,
143
+ encrypted.salt,
144
+ unicodePassword,
145
+ encrypted.kdfParams
146
+ );
147
+
148
+ expect(decrypted).toBe(testData);
149
+ });
150
+ });
151
+
152
+ describe('deriveKeyFromPassword', () => {
153
+ it('should derive consistent key from same password and salt', async () => {
154
+ const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
155
+
156
+ const key1 = await deriveKeyFromPassword(testPassword, salt, DEFAULT_KDF_PARAMS);
157
+ const key2 = await deriveKeyFromPassword(testPassword, salt, DEFAULT_KDF_PARAMS);
158
+
159
+ expect(bytesToHex(key1)).toBe(bytesToHex(key2));
160
+ });
161
+
162
+ it('should derive different keys for different passwords', async () => {
163
+ const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
164
+
165
+ const key1 = await deriveKeyFromPassword('password1', salt, DEFAULT_KDF_PARAMS);
166
+ const key2 = await deriveKeyFromPassword('password2', salt, DEFAULT_KDF_PARAMS);
167
+
168
+ expect(bytesToHex(key1)).not.toBe(bytesToHex(key2));
169
+ });
170
+
171
+ it('should derive different keys for different salts', async () => {
172
+ const salt1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
173
+ const salt2 = new Uint8Array([16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
174
+
175
+ const key1 = await deriveKeyFromPassword(testPassword, salt1, DEFAULT_KDF_PARAMS);
176
+ const key2 = await deriveKeyFromPassword(testPassword, salt2, DEFAULT_KDF_PARAMS);
177
+
178
+ expect(bytesToHex(key1)).not.toBe(bytesToHex(key2));
179
+ });
180
+ });
181
+
182
+ describe('generateEd25519PrivateKey', () => {
183
+ it('should generate a 64-character hex string (32 bytes)', async () => {
184
+ const key = await generateEd25519PrivateKey();
185
+
186
+ expect(key.length).toBe(64);
187
+ expect(/^[0-9a-f]+$/.test(key)).toBe(true);
188
+ });
189
+
190
+ it('should generate different keys each time', async () => {
191
+ const key1 = await generateEd25519PrivateKey();
192
+ const key2 = await generateEd25519PrivateKey();
193
+
194
+ expect(key1).not.toBe(key2);
195
+ });
196
+
197
+ it('should generate valid hex that converts to 32 bytes', async () => {
198
+ const key = await generateEd25519PrivateKey();
199
+ const bytes = hexToBytes(key);
200
+
201
+ expect(bytes.length).toBe(32);
202
+ expect(bytesToHex(bytes)).toBe(key);
203
+ });
204
+ });
205
+ });
206
+
207
+ describe('DEFAULT_KDF_PARAMS', () => {
208
+ it('should have secure Argon2id parameters', () => {
209
+ expect(DEFAULT_KDF_PARAMS.algorithm).toBe('argon2id');
210
+ expect(DEFAULT_KDF_PARAMS.timeCost).toBeGreaterThanOrEqual(3);
211
+ expect(DEFAULT_KDF_PARAMS.memoryCost).toBeGreaterThanOrEqual(65536);
212
+ expect(DEFAULT_KDF_PARAMS.parallelism).toBeGreaterThanOrEqual(1);
213
+ });
214
+ });