1import * as crypto from 'crypto';
2import { createReadStream } from 'fs';
3import type { PathLike } from 'fs';
4import * as fs from 'fs/promises';
5import * as path from 'path';
6import { pipeline } from 'stream/promises';
7
8const STATIC_FOLDER_PATH = path.resolve(__dirname, '..', '.static');
9const RUNTIME_VERSION = '1.0.0';
10const serverHost = process.env.UPDATES_HOST;
11const serverPort = parseInt(process.env.UPDATES_PORT || '', 10);
12
13const urlForBundleFilename = (bundleFilename: any) =>
14  `http://${serverHost}:${serverPort}/static/${bundleFilename}`;
15
16/**
17 * Find the pregenerated bundle corresponding to the string that is expected
18 * in the responses for a given E2E test
19 */
20function findBundlePath(
21  projectRoot: string,
22  platform: string | number,
23  notifyString: string | number
24) {
25  const testUpdateBundlesPath = path.join(projectRoot, 'test-update-bundles');
26  const testUpdateBundlesJsonPath = path.join(testUpdateBundlesPath, 'test-updates.json');
27  const testUpdateBundlesJson = require(testUpdateBundlesJsonPath);
28  const bundleJson = testUpdateBundlesJson[notifyString];
29  if (bundleJson) {
30    const bundleUrl = testUpdateBundlesJson[notifyString][platform];
31    return path.join(testUpdateBundlesPath, bundleUrl);
32  } else {
33    throw new Error(
34      `There is no bundle for notifyString = ${notifyString}. Please add this to the strings used to generate test bundles in 'project.js' (setupBasicAppAsync() or setupAssetsAppAsync())'`
35    );
36  }
37}
38
39/**
40 * Returns all the assets in the updates bundle, both paths and file types
41 */
42function findAssets(projectRoot: string, platform: string | number) {
43  const updatesPath = path.join(projectRoot, 'updates');
44  const updatesJson = require(path.join(updatesPath, 'metadata.json'));
45  const assets = updatesJson.fileMetadata[platform].assets;
46  return assets.map((asset: { path: string; ext: any }) => {
47    return {
48      path: path.join(updatesPath, asset.path),
49      ext: asset.ext,
50    };
51  });
52}
53
54async function shaHash(filePath: PathLike) {
55  const hash = crypto.createHash('sha256');
56  const stream = createReadStream(filePath);
57  await pipeline(stream, hash);
58  return hash.digest('base64url');
59}
60
61/**
62 * Copies a bundle to the location where the test server reads it,
63 * and returns the SHA hash
64 */
65async function copyBundleToStaticFolder(
66  projectRoot: any,
67  filename: string,
68  notifyString: any,
69  platform: any
70) {
71  await fs.mkdir(STATIC_FOLDER_PATH, { recursive: true });
72  const bundleSrcPath = findBundlePath(projectRoot, platform, notifyString);
73  const bundleDestPath = path.join(STATIC_FOLDER_PATH, filename);
74  await fs.copyFile(bundleSrcPath, bundleDestPath);
75  return await shaHash(bundleDestPath);
76}
77
78/**
79 * Copies an asset to the location where the test server reads it,
80 * and returns the SHA hash
81 */
82async function copyAssetToStaticFolder(sourcePath: PathLike, filename: string) {
83  await fs.mkdir(STATIC_FOLDER_PATH, { recursive: true });
84  const destinationPath = path.join(STATIC_FOLDER_PATH, filename);
85  await fs.copyFile(sourcePath, destinationPath);
86  return await shaHash(destinationPath);
87}
88
89/**
90 * Common method used in all the tests to create valid update manifests
91 */
92function getUpdateManifestForBundleFilename(
93  date: { toISOString: () => string },
94  hash: string,
95  key: string,
96  bundleFilename: string,
97  assets: any[],
98  projectRoot: string
99) {
100  const appJson = require(`${projectRoot}/app.json`);
101  return {
102    id: crypto.randomUUID(),
103    createdAt: date.toISOString(),
104    runtimeVersion: RUNTIME_VERSION,
105    launchAsset: {
106      hash,
107      key,
108      contentType: 'application/javascript',
109      url: urlForBundleFilename(bundleFilename),
110    },
111    assets,
112    metadata: {},
113    extra: {
114      expoConfig: appJson.expo,
115    },
116  };
117}
118
119/**
120 * Common method used in all the tests to create valid rollback directives
121 */
122function getRollbackDirective(date: Date) {
123  return {
124    type: 'rollBackToEmbedded',
125    parameters: {
126      commitTime: date.toISOString(),
127    },
128  };
129}
130
131/**
132 * Common method used to create "no update available" directives
133 */
134function getNoUpdateAvailableDirective() {
135  return {
136    type: 'noUpdateAvailable',
137  };
138}
139
140export default {
141  copyBundleToStaticFolder,
142  copyAssetToStaticFolder,
143  findAssets,
144  getUpdateManifestForBundleFilename,
145  getRollbackDirective,
146  getNoUpdateAvailableDirective,
147  serverHost,
148  serverPort,
149};
150