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