1import { createHash } from 'crypto'; 2import { vol } from 'memfs'; 3import pLimit from 'p-limit'; 4import path from 'path'; 5 6import { HashSource } from '../../Fingerprint.types'; 7import { normalizeOptionsAsync } 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 const filePath = 'assets/icon.png'; 27 vol.mkdirSync('/app'); 28 vol.mkdirSync('/app/assets'); 29 vol.writeFileSync(path.join('/app', filePath), '{}'); 30 31 const sources: HashSource[] = [ 32 { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] }, 33 { type: 'file', filePath, reasons: ['icon'] }, 34 ]; 35 36 expect( 37 await createFingerprintFromSourcesAsync(sources, '/app', await normalizeOptionsAsync('/app')) 38 ).toMatchInlineSnapshot(` 39 { 40 "hash": "ca7d58cd60289daa5cddcf99fcaa1d339bfc2c1a", 41 "sources": [ 42 { 43 "contents": "HelloWorld", 44 "hash": "db8ac1c259eb89d4a131b253bacfca5f319d54f2", 45 "id": "foo", 46 "reasons": [ 47 "foo", 48 ], 49 "type": "contents", 50 }, 51 { 52 "filePath": "assets/icon.png", 53 "hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", 54 "reasons": [ 55 "icon", 56 ], 57 "type": "file", 58 }, 59 ], 60 } 61 `); 62 }); 63}); 64 65describe(createFingerprintSourceAsync, () => { 66 it('should merge hash value to original source', async () => { 67 const source: HashSource = { 68 type: 'contents', 69 id: 'foo', 70 contents: 'HelloWorld', 71 reasons: ['foo'], 72 }; 73 const expectedResult = { 74 ...source, 75 hash: 'db8ac1c259eb89d4a131b253bacfca5f319d54f2', 76 }; 77 expect( 78 await createFingerprintSourceAsync( 79 source, 80 pLimit(1), 81 '/app', 82 await normalizeOptionsAsync('/app') 83 ) 84 ).toEqual(expectedResult); 85 }); 86}); 87 88describe(createContentsHashResultsAsync, () => { 89 it('should return {id, hex} result', async () => { 90 const id = 'foo'; 91 const contents = '{}'; 92 const options = await normalizeOptionsAsync('/app'); 93 const result = await createContentsHashResultsAsync( 94 { 95 type: 'contents', 96 id, 97 contents, 98 reasons: [id], 99 }, 100 options 101 ); 102 103 const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex'); 104 expect(result.id).toEqual(id); 105 expect(result.hex).toEqual(expectHex); 106 }); 107}); 108 109describe(createFileHashResultsAsync, () => { 110 afterEach(() => { 111 vol.reset(); 112 }); 113 114 it('should return {id, hex} result', async () => { 115 const filePath = 'assets/icon.png'; 116 const contents = '{}'; 117 const limiter = pLimit(1); 118 const options = await normalizeOptionsAsync('/app'); 119 vol.mkdirSync('/app'); 120 vol.mkdirSync('/app/assets'); 121 vol.writeFileSync(path.join('/app', filePath), contents); 122 123 const result = await createFileHashResultsAsync(filePath, limiter, '/app', options); 124 125 const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex'); 126 expect(result?.id).toEqual(filePath); 127 expect(result?.hex).toEqual(expectHex); 128 }); 129 130 it('should ignore file if it is in options.ignorePaths', async () => { 131 const filePath = 'app.json'; 132 const contents = '{}'; 133 const limiter = pLimit(1); 134 const options = await normalizeOptionsAsync('/app'); 135 options.ignorePaths = ['*.json']; 136 vol.mkdirSync('/app'); 137 vol.writeFileSync(path.join('/app', filePath), contents); 138 139 const result = await createFileHashResultsAsync(filePath, limiter, '/app', options); 140 expect(result).toBe(null); 141 }); 142}); 143 144describe(createDirHashResultsAsync, () => { 145 afterEach(() => { 146 vol.reset(); 147 }); 148 149 it('should return {id, hex} result', async () => { 150 const limiter = pLimit(3); 151 const options = await normalizeOptionsAsync('/app'); 152 const volJSON = { 153 '/app/ios/Podfile': '...', 154 '/app/eas.json': '{}', 155 '/app/app.json': '{}', 156 '/app/android/build.gradle': '...', 157 }; 158 vol.fromJSON(volJSON); 159 const result = await createDirHashResultsAsync('.', limiter, '/app', options); 160 161 expect(result?.id).toEqual('.'); 162 expect(result?.hex).not.toBe(''); 163 }); 164 165 it('should ignore dir if it is in options.ignorePaths', async () => { 166 const limiter = pLimit(3); 167 const options = await normalizeOptionsAsync('/app'); 168 options.ignorePaths = ['ios/**/*', 'android/**/*']; 169 const volJSON = { 170 '/app/ios/Podfile': '...', 171 '/app/eas.json': '{}', 172 '/app/app.json': '{}', 173 '/app/android/build.gradle': '...', 174 }; 175 vol.fromJSON(volJSON); 176 177 const fingerprint1 = await createDirHashResultsAsync('.', limiter, '/app', options); 178 179 vol.reset(); 180 const volJSONIgnoreNativeProjects = { 181 '/app/eas.json': '{}', 182 '/app/app.json': '{}', 183 }; 184 vol.fromJSON(volJSONIgnoreNativeProjects); 185 const fingerprint2 = await createDirHashResultsAsync('.', limiter, '/app', options); 186 expect(fingerprint1).toEqual(fingerprint2); 187 }); 188 189 it('should return stable result from sorted files', async () => { 190 const limiter = pLimit(3); 191 const options = await normalizeOptionsAsync('/app'); 192 const volJSON = { 193 '/app/ios/Podfile': '...', 194 '/app/eas.json': '{}', 195 '/app/app.json': '{}', 196 '/app/android/build.gradle': '...', 197 }; 198 vol.fromJSON(volJSON); 199 const result = await createDirHashResultsAsync('.', limiter, '/app', options); 200 201 vol.reset(); 202 const sortedVolJSON = { 203 '/app/app.json': '{}', 204 '/app/eas.json': '{}', 205 '/app/android/build.gradle': '...', 206 '/app/ios/Podfile': '...', 207 }; 208 vol.fromJSON(sortedVolJSON); 209 const sortedResult = await createDirHashResultsAsync('.', limiter, '/app', options); 210 211 expect(result?.id).toEqual(sortedResult?.id); 212 expect(result?.hex).toEqual(sortedResult?.hex); 213 }); 214}); 215 216describe(createSourceId, () => { 217 it(`should use filePath as id for file or dir`, () => { 218 const fileSource: HashSource = { 219 type: 'file', 220 filePath: '/app/app.json', 221 reasons: ['expoConfig'], 222 }; 223 expect(createSourceId(fileSource)).toBe('/app/app.json'); 224 225 const dirSource: HashSource = { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] }; 226 expect(createSourceId(dirSource)).toBe('/app/ios'); 227 }); 228 229 it(`should use given id for contents`, () => { 230 const source: HashSource = { 231 type: 'contents', 232 id: 'foo', 233 contents: 'HelloWorld', 234 reasons: ['foo'], 235 }; 236 expect(createSourceId(source)).toBe('foo'); 237 }); 238}); 239