1import { createHash } from 'crypto'; 2import { vol } from 'memfs'; 3import pLimit from 'p-limit'; 4import path from 'path'; 5 6import { HashSource } from '../../Fingerprint.types'; 7import { normalizeOptions } from '../../Options'; 8import { 9 createContentsHashResultsAsync, 10 createDirHashResultsAsync, 11 createFileHashResultsAsync, 12 createFingerprintFromSourcesAsync, 13 createFingerprintSourceAsync, 14 createSourceId, 15} from '../Hash'; 16 17jest.mock('fs'); 18jest.mock('fs/promises'); 19 20describe(createFingerprintFromSourcesAsync, () => { 21 afterEach(() => { 22 vol.reset(); 23 }); 24 25 it('snapshot', async () => { 26 vol.mkdirSync('/app'); 27 vol.writeFileSync(path.join('/app', 'app.json'), '{}'); 28 29 const sources: HashSource[] = [ 30 { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] }, 31 { type: 'file', filePath: 'app.json', reasons: ['expoConfig'] }, 32 ]; 33 34 expect(await createFingerprintFromSourcesAsync(sources, '/app', normalizeOptions())) 35 .toMatchInlineSnapshot(` 36 { 37 "hash": "ec7d81780f735d5e289b27cdcc04a6c99d2621dc", 38 "sources": [ 39 { 40 "contents": "HelloWorld", 41 "hash": "db8ac1c259eb89d4a131b253bacfca5f319d54f2", 42 "id": "foo", 43 "reasons": [ 44 "foo", 45 ], 46 "type": "contents", 47 }, 48 { 49 "filePath": "app.json", 50 "hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", 51 "reasons": [ 52 "expoConfig", 53 ], 54 "type": "file", 55 }, 56 ], 57 } 58 `); 59 }); 60}); 61 62describe(createFingerprintSourceAsync, () => { 63 it('should merge hash value to original source', async () => { 64 const source: HashSource = { 65 type: 'contents', 66 id: 'foo', 67 contents: 'HelloWorld', 68 reasons: ['foo'], 69 }; 70 const expectedResult = { 71 ...source, 72 hash: 'db8ac1c259eb89d4a131b253bacfca5f319d54f2', 73 }; 74 expect( 75 await createFingerprintSourceAsync(source, pLimit(1), '/app', normalizeOptions()) 76 ).toEqual(expectedResult); 77 }); 78}); 79 80describe(createContentsHashResultsAsync, () => { 81 it('should return {id, hex} result', async () => { 82 const id = 'foo'; 83 const contents = '{}'; 84 const options = normalizeOptions(); 85 const result = await createContentsHashResultsAsync( 86 { 87 type: 'contents', 88 id, 89 contents, 90 reasons: [id], 91 }, 92 options 93 ); 94 95 const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex'); 96 expect(result.id).toEqual(id); 97 expect(result.hex).toEqual(expectHex); 98 }); 99}); 100 101describe(createFileHashResultsAsync, () => { 102 afterEach(() => { 103 vol.reset(); 104 }); 105 106 it('should return {id, hex} result', async () => { 107 const filePath = 'app.json'; 108 const contents = '{}'; 109 const limiter = pLimit(1); 110 const options = normalizeOptions(); 111 vol.mkdirSync('/app'); 112 vol.writeFileSync(path.join('/app', filePath), contents); 113 114 const result = await createFileHashResultsAsync(filePath, limiter, '/app', options); 115 116 const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex'); 117 expect(result.id).toEqual(filePath); 118 expect(result.hex).toEqual(expectHex); 119 }); 120}); 121 122describe(createDirHashResultsAsync, () => { 123 afterEach(() => { 124 vol.reset(); 125 }); 126 127 it('should return {id, hex} result', async () => { 128 const limiter = pLimit(3); 129 const options = normalizeOptions(); 130 const volJSON = { 131 '/app/ios/Podfile': '...', 132 '/app/eas.json': '{}', 133 '/app/app.json': '{}', 134 '/app/android/build.gradle': '...', 135 }; 136 vol.fromJSON(volJSON); 137 const result = await createDirHashResultsAsync('.', limiter, '/app', options); 138 139 expect(result?.id).toEqual('.'); 140 expect(result?.hex).not.toBe(''); 141 }); 142 143 it('should return stable result from sorted files', async () => { 144 const limiter = pLimit(3); 145 const options = normalizeOptions(); 146 const volJSON = { 147 '/app/ios/Podfile': '...', 148 '/app/eas.json': '{}', 149 '/app/app.json': '{}', 150 '/app/android/build.gradle': '...', 151 }; 152 vol.fromJSON(volJSON); 153 const result = await createDirHashResultsAsync('.', limiter, '/app', options); 154 155 vol.reset(); 156 const sortedVolJSON = { 157 '/app/app.json': '{}', 158 '/app/eas.json': '{}', 159 '/app/android/build.gradle': '...', 160 '/app/ios/Podfile': '...', 161 }; 162 vol.fromJSON(sortedVolJSON); 163 const sortedResult = await createDirHashResultsAsync('.', limiter, '/app', options); 164 165 expect(result?.id).toEqual(sortedResult?.id); 166 expect(result?.hex).toEqual(sortedResult?.hex); 167 }); 168}); 169 170describe(createSourceId, () => { 171 it(`should use filePath as id for file or dir`, () => { 172 const fileSource: HashSource = { 173 type: 'file', 174 filePath: '/app/app.json', 175 reasons: ['expoConfig'], 176 }; 177 expect(createSourceId(fileSource)).toBe('/app/app.json'); 178 179 const dirSource: HashSource = { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] }; 180 expect(createSourceId(dirSource)).toBe('/app/ios'); 181 }); 182 183 it(`should use given id for contents`, () => { 184 const source: HashSource = { 185 type: 'contents', 186 id: 'foo', 187 contents: 'HelloWorld', 188 reasons: ['foo'], 189 }; 190 expect(createSourceId(source)).toBe('foo'); 191 }); 192}); 193