import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree'; import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants'; import {expect} from '@webex/test-helper-chai'; // Define a type for the leaf data items used in tests type LeafDataItem = { type: string; id: number; version: number; }; describe('HashTree', () => { it('should initialize with empty leaves and hashes', () => { const leafData: LeafDataItem[] = []; const numLeaves = 4; const hashTree = new HashTree(leafData, numLeaves); expect(hashTree.leaves).to.deep.equal(new Array(numLeaves).fill(null).map(() => ({}))); expect(hashTree.leafHashes).to.deep.equal(new Array(numLeaves).fill(EMPTY_HASH)); expect(hashTree.getLeafCount()).to.equal(numLeaves); expect(hashTree.getTotalItemCount()).to.equal(0); }); it('constructor should allow 0 leaves', () => { const leafData: LeafDataItem[] = []; const numLeaves = 0; const hashTree = new HashTree(leafData, numLeaves); expect(hashTree.getLeafCount()).to.equal(0); expect(hashTree.getRootHash()).to.equal(EMPTY_HASH); expect(hashTree.getHashes()).to.deep.equal([EMPTY_HASH]); }); it('number of leaves must be 0 or a power of 2', () => { const leafData: LeafDataItem[] = []; const numLeaves = 3; // Not a power of 2 expect(() => new HashTree(leafData, numLeaves)).to.throw( 'Number of leaves must be a power of 2, saw 3' ); const numLeavesNegative = -1; expect(() => new HashTree(leafData, numLeavesNegative)).to.throw( 'Number of leaves must be a power of 2, saw -1' ); }); it('should have the correct hashes after putting ObjectIds using constructor', () => { const oids: LeafDataItem[] = [ {type: 'participant', id: 1, version: 3}, // Hashes to bucket 1 % 4 = 1 ]; // numLeaves is 4. Item id 1 % 4 = 1. So, leafHashes[1] will be updated. // leafHashes[0], leafHashes[2], leafHashes[3] remain EMPTY_HASH. const tree = new HashTree(oids, 4); // These are the expected hash values from the Java reference for a similar structure. // The actual values depend on the specific XXHash128 implementation and input serialization. // For this test, we'll use the previously provided values, assuming they are correct for the TS implementation. expect(tree.getHashes()).to.deep.equal([ '24a75d115a0a90ddb376a02b435c780f', // Root hash '457eeb22808eadfcff92ee47d67acbbf', // Internal node (children: leaf 0, leaf 1) 'b113a76304e3a7121afecfe1606ee1c1', // Internal node (children: leaf 2, leaf 3) EMPTY_HASH, // Leaf 0 hash (empty) '42df811f5a902c5b6bfcf50c7004e275', // Leaf 1 hash (for item {type: 'participant', id: 1, version: 3}) EMPTY_HASH, // Leaf 2 hash (empty) EMPTY_HASH, // Leaf 3 hash (empty) ]); expect(tree.getRootHash()).to.equal('24a75d115a0a90ddb376a02b435c780f'); }); it('should have the correct hashes after putting multiple ObjectIds using constructor', () => { const oids: LeafDataItem[] = [ {type: 'typeA', id: 1, version: 3}, // Leaf 1 (1 % 4 = 1) {type: 'typeA', id: 6, version: 2}, // Leaf 2 (6 % 4 = 2) {type: 'typeA', id: 7, version: 1}, // Leaf 3 (7 % 4 = 3) {type: 'typeB', id: 11, version: 4}, // Leaf 3 (11 % 4 = 3) ]; const tree = new HashTree(oids, 4); // Corrected expected hashes based on the test failure output expect(tree.getHashes()).to.deep.equal([ 'c8415198d4abca6f885fe974e9b3729d', // Root '457eeb22808eadfcff92ee47d67acbbf', // Internal node (L0, L1) '5c9ba182a069c16a77a1928fce52dad8', // Internal node (L2, L3) EMPTY_HASH, // Leaf 0 (empty) '42df811f5a902c5b6bfcf50c7004e275', // Leaf 1 (item id 1) 'feb384d8ac6374ffdbee92a9f48f2b40', // Leaf 2 (item id 6) 'ebfa4f7e104e1e30fbb6b8857ccb685d', // Leaf 3 (items id 7, 11) ]); expect(tree.getRootHash()).to.equal('c8415198d4abca6f885fe974e9b3729d'); }); it('should putItems and compute hashes correctly', () => { const initialLeafData: LeafDataItem[] = []; const numLeaves = 4; const hashTree = new HashTree(initialLeafData, numLeaves); const itemsToPut: LeafDataItem[] = [ {type: 'participant', id: 1, version: 1}, // bucket 1 {type: 'participant', id: 2, version: 1}, // bucket 2 ]; const results = hashTree.putItems(itemsToPut); expect(results).to.deep.equal([true, true]); expect(hashTree.leaves[1]['participant'][1]).to.deep.equal({ type: 'participant', id: 1, version: 1, }); expect(hashTree.leaves[2]['participant'][2]).to.deep.equal({ type: 'participant', id: 2, version: 1, }); expect(hashTree.leafHashes[0]).to.equal(EMPTY_HASH); expect(hashTree.leafHashes[1]).to.not.equal(EMPTY_HASH); expect(hashTree.leafHashes[2]).to.not.equal(EMPTY_HASH); expect(hashTree.leafHashes[3]).to.equal(EMPTY_HASH); expect(hashTree.getTotalItemCount()).to.equal(2); }); it('putItem should add a single item and update hash', () => { const hashTree = new HashTree([], 2); const item: LeafDataItem = {type: 'data', id: 3, version: 1}; // bucket 1 const result = hashTree.putItem(item); expect(result).to.be.true; expect(hashTree.leaves[1]['data'][3]).to.deep.equal(item); expect(hashTree.leafHashes[1]).to.not.equal(EMPTY_HASH); expect(hashTree.getTotalItemCount()).to.equal(1); const itemSameVersion = {type: 'data', id: 3, version: 1}; const resultSame = hashTree.putItem(itemSameVersion); expect(resultSame).to.be.false; // Not updated as version is not newer const itemNewerVersion = {type: 'data', id: 3, version: 2}; const resultNewer = hashTree.putItem(itemNewerVersion); expect(resultNewer).to.be.true; expect(hashTree.leaves[1]['data'][3].version).to.equal(2); }); it('putItem should return false for tree with 0 leaves', () => { const hashTree = new HashTree([], 0); const item: LeafDataItem = {type: 'data', id: 1, version: 1}; expect(hashTree.putItem(item)).to.be.false; }); it('putItems should return array of false for tree with 0 leaves if items are provided', () => { const hashTree = new HashTree([], 0); const items: LeafDataItem[] = [{type: 'data', id: 1, version: 1}]; expect(hashTree.putItems(items)).to.deep.equal([false]); }); it('should have correct root hash after putting one item', () => { const leafData: LeafDataItem[] = [{type: 'participant', id: 1, version: 10}]; // bucket 1 (1 % 2 = 1) const numLeaves = 2; const hashTree = new HashTree(leafData, numLeaves); expect(hashTree.leaves[1]['participant'][1]).to.deep.equal({ type: 'participant', id: 1, version: 10, }); // This hash is from the original test. expect(hashTree.getRootHash()).to.equal('e1cb70c75b488d87cbc8f74934a4290b'); }); it('removeItem should remove an item and update hash', () => { const items: LeafDataItem[] = [{type: 'p', id: 1, version: 1}]; const hashTree = new HashTree(items, 2); // item in bucket 1 expect(hashTree.getTotalItemCount()).to.equal(1); const oldRootHash = hashTree.getRootHash(); const result = hashTree.removeItem({type: 'p', id: 1, version: 1}); expect(result).to.be.true; expect(hashTree.getTotalItemCount()).to.equal(0); expect(hashTree.leaves[1]['p']).to.be.undefined; expect(hashTree.leafHashes[1]).to.equal(EMPTY_HASH); expect(hashTree.getRootHash()).to.not.equal(oldRootHash); // After removing the only item, it should be like an empty tree with 2 leaves const emptyTree = new HashTree([], 2); expect(hashTree.getRootHash()).to.equal(emptyTree.getRootHash()); const resultNotFound = hashTree.removeItem({type: 'p', id: 1, version: 1}); expect(resultNotFound).to.be.false; }); it('removeItem should only remove if version is <= existing (as per new logic)', () => { const itemV1 = {type: 'test', id: 5, version: 1}; // bucket 1 (5%2=1) const hashTree = new HashTree([itemV1], 2); // Try to remove with older version - should fail if strict "version must be >=" is used for removal item // The current removeItem logic: existingItem.version <= item.version for removal let removed = hashTree.removeItem({type: 'test', id: 5, version: 0}); expect(removed).to.be.false; removed = hashTree.removeItem({type: 'test', id: 5, version: 1}); // same version expect(removed).to.be.true; expect(hashTree.getTotalItemCount()).to.equal(0); hashTree.putItem(itemV1); // re-add expect(hashTree.getTotalItemCount()).to.equal(1); removed = hashTree.removeItem({type: 'test', id: 5, version: 2}); // newer version in request expect(removed).to.be.true; expect(hashTree.getTotalItemCount()).to.equal(0); }); it('removeItem should return false for tree with 0 leaves', () => { const hashTree = new HashTree([], 0); const item: LeafDataItem = {type: 'data', id: 1, version: 1}; expect(hashTree.removeItem(item)).to.be.false; }); it('removeItems should process multiple items', () => { const items: LeafDataItem[] = [ {type: 'a', id: 1, version: 2}, // bucket 1 {type: 'b', id: 2, version: 2}, // bucket 0 ]; const hashTree = new HashTree(items, 2); expect(hashTree.getTotalItemCount()).to.equal(2); const itemsToRemove: LeafDataItem[] = [ {type: 'a', id: 1, version: 3}, // remove with newer version (original logic) {type: 'b', id: 2, version: 1}, // attempt remove with older version (should fail by original logic) {type: 'c', id: 3, version: 1}, // item not present ]; const results = hashTree.removeItems(itemsToRemove); expect(results).to.deep.equal([true, false, false]); expect(hashTree.getTotalItemCount()).to.equal(1); // item 'b' should remain expect(hashTree.leaves[1]['a']).to.be.undefined; expect(hashTree.leaves[0]['b'][2]).to.deep.equal({type: 'b', id: 2, version: 2}); }); it('removeItems should return array of false for tree with 0 leaves if items are provided', () => { const hashTree = new HashTree([], 0); const items: LeafDataItem[] = [{type: 'data', id: 1, version: 1}]; expect(hashTree.removeItems(items)).to.deep.equal([false]); }); it('updateItems should update and remove multiple items correctly', () => { const initialItems: LeafDataItem[] = [ {type: 'participant', id: 1, version: 1}, // bucket 1 {type: 'self', id: 2, version: 1}, // bucket 0 {type: 'locus', id: 3, version: 1}, // bucket 1 ]; const hashTree = new HashTree(initialItems, 2); expect(hashTree.getTotalItemCount()).to.equal(3); const updates: any = [ {operation: 'update', item: {type: 'participant', id: 1, version: 2}}, // update existing {operation: 'remove', item: {type: 'locus', id: 3, version: 1}}, // remove existing {operation: 'update', item: {type: 'mediashare', id: 4, version: 1}}, // add new (bucket 0) {operation: 'remove', item: {type: 'participant', id: 99, version: 1}}, // remove non-existent ]; const results = hashTree.updateItems(updates); expect(results).to.deep.equal([true, true, true, false]); expect(hashTree.getTotalItemCount()).to.equal(3); // participant (updated), self (unchanged), mediashare (added); locus removed // Verify the updates expect(hashTree.leaves[1]['participant'][1].version).to.equal(2); // updated expect(hashTree.leaves[0]['self'][2]).to.deep.equal({type: 'self', id: 2, version: 1}); // unchanged expect(hashTree.leaves[0]['mediashare'][4]).to.deep.equal({ type: 'mediashare', id: 4, version: 1, }); // added expect(hashTree.leaves[1]['locus']).to.be.undefined; // removed // Verify hashes were updated expect(hashTree.leafHashes[0]).to.not.equal(EMPTY_HASH); expect(hashTree.leafHashes[1]).to.not.equal(EMPTY_HASH); }); it('updateItems should return array of false for tree with 0 leaves', () => { const hashTree = new HashTree([], 0); const updates: any = [ {operation: 'update', item: {type: 'participant', id: 1, version: 1}}, {operation: 'remove', item: {type: 'self', id: 2, version: 1}}, ]; expect(hashTree.updateItems(updates)).to.deep.equal([false, false]); }); it('updateItems should handle empty updates array', () => { const hashTree = new HashTree([{type: 'participant', id: 1, version: 1}], 2); const results = hashTree.updateItems([]); expect(results).to.deep.equal([]); expect(hashTree.getTotalItemCount()).to.equal(1); }); it('updateItems should only update hashes for affected leaves', () => { const hashTree = new HashTree([], 4); const initialHash0 = hashTree.leafHashes[0]; const initialHash1 = hashTree.leafHashes[1]; const initialHash2 = hashTree.leafHashes[2]; const initialHash3 = hashTree.leafHashes[3]; const updates: any = [ {operation: 'update', item: {type: 'participant', id: 1, version: 1}}, // bucket 1 ]; hashTree.updateItems(updates); // Only leaf 1 should have changed hash expect(hashTree.leafHashes[0]).to.equal(initialHash0); expect(hashTree.leafHashes[1]).to.not.equal(initialHash1); expect(hashTree.leafHashes[2]).to.equal(initialHash2); expect(hashTree.leafHashes[3]).to.equal(initialHash3); }); it('updateItems should respect version control for updates', () => { const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 2); const updates: any = [ {operation: 'update', item: {type: 'participant', id: 1, version: 4}}, // older version {operation: 'update', item: {type: 'participant', id: 1, version: 5}}, // same version {operation: 'update', item: {type: 'participant', id: 1, version: 6}}, // newer version ]; const results = hashTree.updateItems(updates); expect(results).to.deep.equal([false, false, true]); expect(hashTree.leaves[1]['participant'][1].version).to.equal(6); }); it('updateItems should respect version control for removes', () => { const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 2); // Try to remove with older version - should fail let updates: any = [{operation: 'remove', item: {type: 'participant', id: 1, version: 4}}]; let results = hashTree.updateItems(updates); expect(results).to.deep.equal([false]); expect(hashTree.getTotalItemCount()).to.equal(1); // still there // Try with same version - should succeed updates = [{operation: 'remove', item: {type: 'participant', id: 1, version: 5}}]; results = hashTree.updateItems(updates); expect(results).to.deep.equal([true]); expect(hashTree.getTotalItemCount()).to.equal(0); }); it('updateItems should handle multiple operations on same leaf efficiently', () => { const hashTree = new HashTree([], 2); const updates: any = [ {operation: 'update', item: {type: 'participant', id: 1, version: 1}}, // bucket 1 {operation: 'update', item: {type: 'self', id: 3, version: 1}}, // bucket 1 {operation: 'update', item: {type: 'locus', id: 5, version: 1}}, // bucket 1 ]; const results = hashTree.updateItems(updates); expect(results).to.deep.equal([true, true, true]); expect(hashTree.getTotalItemCount()).to.equal(3); // All items should be in leaf 1 expect(hashTree.leaves[1]['participant'][1]).to.exist; expect(hashTree.leaves[1]['self'][3]).to.exist; expect(hashTree.leaves[1]['locus'][5]).to.exist; }); it('returns the correct root hash for an empty tree (0 leaves)', () => { const hashTree = new HashTree([], 0); expect(hashTree.getRootHash()).to.equal(EMPTY_HASH); }); it('returns the correct root hash for an empty tree with 2 leaves', () => { const hashTree = new HashTree([], 2); // This hash is from the original test. expect(hashTree.getRootHash()).to.equal('b113a76304e3a7121afecfe1606ee1c1'); }); it('returns the correct root hash for an empty tree with 4 leaves', () => { const hashTree = new HashTree([], 4); // This hash is from the original test. expect(hashTree.getRootHash()).to.equal('b5df9b92242752424d87053a14e6222d'); }); describe('getTotalItemCount', () => { it('should return 0 for an empty tree', () => { const tree = new HashTree([], 4); expect(tree.getTotalItemCount()).to.equal(0); }); it('should return 0 for a tree with 0 leaves', () => { const tree = new HashTree([], 0); expect(tree.getTotalItemCount()).to.equal(0); }); it('should return correct count for a tree with single item', () => { const tree = new HashTree([{type: 'participant', id: 1, version: 1}], 2); expect(tree.getTotalItemCount()).to.equal(1); }); it('should return correct count for multiple items in different leaves', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 0, version: 1}, // leaf 0 {type: 'self', id: 1, version: 1}, // leaf 1 {type: 'locus', id: 2, version: 1}, // leaf 0 {type: 'mediashare', id: 3, version: 1}, // leaf 1 ]; const tree = new HashTree(items, 2); expect(tree.getTotalItemCount()).to.equal(4); }); it('should return correct count for multiple items of different types in same leaf', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 1, version: 1}, // leaf 1 {type: 'self', id: 3, version: 1}, // leaf 1 {type: 'locus', id: 5, version: 1}, // leaf 1 ]; const tree = new HashTree(items, 2); expect(tree.getTotalItemCount()).to.equal(3); }); it('should maintain correct count after resize', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 0, version: 1}, {type: 'self', id: 1, version: 1}, {type: 'locus', id: 2, version: 1}, ]; const tree = new HashTree(items, 2); expect(tree.getTotalItemCount()).to.equal(3); tree.resize(4); expect(tree.getTotalItemCount()).to.equal(3); // Count should remain same after resize }); it('should return 0 after resizing to 0 leaves', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 1, version: 1}, {type: 'self', id: 2, version: 1}, ]; const tree = new HashTree(items, 2); expect(tree.getTotalItemCount()).to.equal(2); tree.resize(0); expect(tree.getTotalItemCount()).to.equal(0); }); }); describe('getLeafData', () => { it('should return items from a specific leaf', () => { const items: LeafDataItem[] = [ {type: 't1', id: 0, version: 1}, // leaf 0 {type: 't2', id: 1, version: 1}, // leaf 1 {type: 't1', id: 2, version: 1}, // leaf 0 ]; const tree = new HashTree(items, 2); const leaf0Data = tree.getLeafData(0); expect(leaf0Data).to.have.deep.members([ {type: 't1', id: 0, version: 1}, {type: 't1', id: 2, version: 1}, ]); expect(leaf0Data.length).to.equal(2); const leaf1Data = tree.getLeafData(1); expect(leaf1Data).to.have.deep.members([{type: 't2', id: 1, version: 1}]); expect(leaf1Data.length).to.equal(1); }); it('should return empty array for invalid leaf index or empty leaf', () => { const tree = new HashTree([{type: 't', id: 0, version: 1}], 2); // item in leaf 0 expect(tree.getLeafData(1)).to.deep.equal([]); // leaf 1 is empty expect(tree.getLeafData(2)).to.deep.equal([]); // invalid index expect(tree.getLeafData(-1)).to.deep.equal([]); // invalid index }); it('should return empty array for tree with 0 leaves', () => { const tree = new HashTree([], 0); expect(tree.getLeafData(0)).to.deep.equal([]); }); }); describe('resize', () => { it('should resize the tree and redistribute items', () => { const items: LeafDataItem[] = [ {type: 'a', id: 0, version: 1}, // old leaf 0 (0%2=0) {type: 'b', id: 1, version: 1}, // old leaf 1 (1%2=1) {type: 'c', id: 2, version: 1}, // old leaf 0 (2%2=0) {type: 'd', id: 3, version: 1}, // old leaf 1 (3%2=1) ]; const tree = new HashTree(items, 2); expect(tree.getLeafCount()).to.equal(2); expect(tree.getTotalItemCount()).to.equal(4); const originalRootHash = tree.getRootHash(); const resized = tree.resize(4); expect(resized).to.be.true; expect(tree.getLeafCount()).to.equal(4); expect(tree.getTotalItemCount()).to.equal(4); // count should remain same // Check redistribution // id:0 -> 0%4 = 0 // id:1 -> 1%4 = 1 // id:2 -> 2%4 = 2 // id:3 -> 3%4 = 3 expect(tree.getLeafData(0)).to.deep.include({type: 'a', id: 0, version: 1}); expect(tree.getLeafData(1)).to.deep.include({type: 'b', id: 1, version: 1}); expect(tree.getLeafData(2)).to.deep.include({type: 'c', id: 2, version: 1}); expect(tree.getLeafData(3)).to.deep.include({type: 'd', id: 3, version: 1}); expect(tree.getRootHash()).to.not.equal(originalRootHash); // Hash should change }); it('should return false if size does not change', () => { const tree = new HashTree([], 4); expect(tree.resize(4)).to.be.false; }); it('should throw error for invalid new number of leaves', () => { const tree = new HashTree([], 4); expect(() => tree.resize(3)).to.throw('New number of leaves must be 0 or a power of 2'); }); it('should handle resize to 0 leaves', () => { const items: LeafDataItem[] = [{type: 'a', id: 0, version: 1}]; const tree = new HashTree(items, 2); expect(tree.getTotalItemCount()).to.equal(1); tree.resize(0); expect(tree.getLeafCount()).to.equal(0); expect(tree.getTotalItemCount()).to.equal(0); expect(tree.getRootHash()).to.equal(EMPTY_HASH); expect(tree.leaves.length).to.equal(0); expect(tree.leafHashes.length).to.equal(0); }); it('should handle resize from 0 leaves', () => { const tree = new HashTree([], 0); tree.resize(2); expect(tree.getLeafCount()).to.equal(2); expect(tree.getTotalItemCount()).to.equal(0); const emptyTree = new HashTree([], 2); expect(tree.getRootHash()).to.equal(emptyTree.getRootHash()); }); }); describe('diffHashes', () => { it('should return empty array if hashes are identical', () => { const items: LeafDataItem[] = [{type: 'x', id: 1, version: 1}]; const tree1 = new HashTree(items, 2); const tree2 = new HashTree(items, 2); expect(tree1.diffHashes(tree2.getHashes())).to.deep.equal([]); }); it('should return differing leaf indices', () => { const tree1 = new HashTree([{type: 'x', id: 0, version: 1}], 4); // item in leaf 0 const tree2 = new HashTree([{type: 'y', id: 1, version: 1}], 4); // item in leaf 1 // tree1: leaf 0 has item, leaves 1,2,3 empty // tree2: leaf 1 has item, leaves 0,2,3 empty // Expected diffs: leaf 0 (present in 1, not in 2), leaf 1 (present in 2, not in 1) const diff = tree1.diffHashes(tree2.getHashes()); expect(diff).to.include.members([0, 1]); // If one leaf's hash is EMPTY_HASH and the other's is a computed hash, they are different. }); it('should return all leaf indices if externalHashes is for a different structure (e.g. too short)', () => { const tree = new HashTree([{type: 'x', id: 0, version: 1}], 4); const externalHashesShort = [EMPTY_HASH, EMPTY_HASH]; // Too short for 4 leaves + internal nodes expect(tree.diffHashes(externalHashesShort)).to.deep.equal([0, 1, 2, 3]); }); it('should handle diff for 0-leaf trees', () => { const tree0 = new HashTree([], 0); expect(tree0.diffHashes([EMPTY_HASH])).to.deep.equal([]); expect(tree0.diffHashes(['some_other_hash'])).to.deep.equal([]); // No leaves to differ const tree2 = new HashTree([], 2); // Comparing a 0-leaf tree with a 2-leaf tree's hashes expect(tree0.diffHashes(tree2.getHashes())).to.deep.equal([]); }); it('should correctly identify differences when one leaf changes', () => { const initialItems: LeafDataItem[] = [ {type: 'a', id: 0, version: 1}, // leaf 0 {type: 'b', id: 1, version: 1}, // leaf 1 ]; const tree1 = new HashTree(initialItems, 2); const tree1Hashes = tree1.getHashes(); const modifiedItems: LeafDataItem[] = [ {type: 'a', id: 0, version: 1}, // leaf 0 (same) {type: 'b', id: 1, version: 2}, // leaf 1 (changed version) ]; const tree2 = new HashTree(modifiedItems, 2); const diff1_2 = tree1.diffHashes(tree2.getHashes()); expect(diff1_2).to.deep.equal([1]); // Leaf 1 should differ const diff2_1 = tree2.diffHashes(tree1Hashes); expect(diff2_1).to.deep.equal([1]); }); }); describe('computeTreeHashes', () => { it('should return [EMPTY_HASH] for tree with 0 leaves', () => { const hashTree = new HashTree([], 0); const hashes = hashTree.computeTreeHashes(); expect(hashes).to.deep.equal([EMPTY_HASH]); }); it('should compute correct hashes for a known set of leaf hashes', () => { const leafHashes = [ 'aefa055a9b82c4c4ae10ac8ed1f61a30', '99aa06d3014798d86001c324468d497f', '99aa06d3014798d86001c324468d497f', 'c770ab632efcea7569a6e35c2f7cf5da', 'eedafc8238926775cee1fbb5cee030ff', 'dae3c2ec206d7d5967bfae01913c4a76', '5301845214af2e8b70c7b54a72565dcf', '99aa06d3014798d86001c324468d497f', ]; const expectedAllHashes = [ 'ba1be9f757eae740753f887d76b7c405', '0e027dc86d522c9cb61e3e20b33e0cb7', 'f6df8f800fac269c448b7725021a9dbb', '803dde85957d497718837fb7e36342f8', '55ed9b63802480f79698432a84a4e5f8', 'fa25dc2d64096c3b92f6701572060569', 'def77631d182a9b74523b218f0de771f', 'aefa055a9b82c4c4ae10ac8ed1f61a30', // - leaves start here '99aa06d3014798d86001c324468d497f', '99aa06d3014798d86001c324468d497f', 'c770ab632efcea7569a6e35c2f7cf5da', 'eedafc8238926775cee1fbb5cee030ff', 'dae3c2ec206d7d5967bfae01913c4a76', '5301845214af2e8b70c7b54a72565dcf', '99aa06d3014798d86001c324468d497f', ]; const hashTree = new HashTree([], leafHashes.length); hashTree.leafHashes = leafHashes; const computedHashes = hashTree.computeTreeHashes(); expect(computedHashes).to.deep.equal(expectedAllHashes); }); }); describe('computeLeafHash', () => { it('should compute the correct hash when the hash starts with a zero', () => { const item: LeafDataItem = {type: 'self', id: 74, version: 1}; // Chosen to produce a hash starting with zero const hashTree = new HashTree([], 2); hashTree.putItem(item); hashTree.computeLeafHash(0); const leafHash = hashTree.leafHashes[0]; expect(leafHash.startsWith('0')).to.be.true; expect(leafHash).equal('0525d6616d0f20119293c0bf2c818e8a'); }); it('should not crash for invalid leaf index', () => { const hashTree = new HashTree([], 2); expect(() => hashTree.computeLeafHash(-1)).to.not.throw(); expect(() => hashTree.computeLeafHash(2)).to.not.throw(); }); }); describe('getItemVersion', () => { it('should return version when item exists', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 1, version: 5}, {type: 'self', id: 2, version: 10}, ]; const hashTree = new HashTree(items, 4); expect(hashTree.getItemVersion(1, 'participant')).to.equal(5); expect(hashTree.getItemVersion(2, 'self')).to.equal(10); }); it('should return undefined when item does not exist', () => { const items: LeafDataItem[] = [{type: 'participant', id: 1, version: 5}]; const hashTree = new HashTree(items, 4); expect(hashTree.getItemVersion(999, 'participant')).to.be.undefined; }); it('should return undefined when type does not match', () => { const items: LeafDataItem[] = [{type: 'participant', id: 1, version: 5}]; const hashTree = new HashTree(items, 4); expect(hashTree.getItemVersion(1, 'self')).to.be.undefined; }); it('should return undefined for tree with 0 leaves', () => { const hashTree = new HashTree([], 0); expect(hashTree.getItemVersion(1, 'participant')).to.be.undefined; }); it('should return correct version when multiple items exist in same leaf', () => { const items: LeafDataItem[] = [ {type: 'participant', id: 1, version: 3}, // leaf 1 (1 % 2 = 1) {type: 'self', id: 3, version: 7}, // leaf 1 (3 % 2 = 1) {type: 'locus', id: 5, version: 12}, // leaf 1 (5 % 2 = 1) ]; const hashTree = new HashTree(items, 2); expect(hashTree.getItemVersion(1, 'participant')).to.equal(3); expect(hashTree.getItemVersion(3, 'self')).to.equal(7); expect(hashTree.getItemVersion(5, 'locus')).to.equal(12); }); it('should return updated version after item is updated', () => { const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 4); expect(hashTree.getItemVersion(1, 'participant')).to.equal(5); hashTree.putItem({type: 'participant', id: 1, version: 10}); expect(hashTree.getItemVersion(1, 'participant')).to.equal(10); }); it('should return undefined after item is removed', () => { const hashTree = new HashTree([{type: 'participant', id: 1, version: 5}], 4); expect(hashTree.getItemVersion(1, 'participant')).to.equal(5); hashTree.removeItem({type: 'participant', id: 1, version: 5}); expect(hashTree.getItemVersion(1, 'participant')).to.be.undefined; }); }); });