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