UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

321 lines 17.3 kB
// SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai'; import { after, before, beforeEach, afterEach, describe, it } from 'mocha'; import each from 'mocha-each'; import fs from 'node:fs'; import sinon from 'sinon'; import { container } from 'tsyringe-neo'; import { platform } from 'node:process'; import { CraneDependencyManager } from '../../../../../src/core/dependency-managers/crane-dependency-manager.js'; import { getTestCacheDirectory, getTemporaryDirectory } from '../../../../test-utility.js'; import * as version from '../../../../../version.js'; import { PathEx } from '../../../../../src/business/utils/path-ex.js'; import * as constants from '../../../../../src/core/constants.js'; import { OperatingSystem } from '../../../../../src/business/utils/operating-system.js'; import { InjectTokens } from '../../../../../src/core/dependency-injection/inject-tokens.js'; import { ShellRunner } from '../../../../../src/core/shell-runner.js'; // Test data constants const CRANE_VERSION = version.CRANE_VERSION.replace(/^v/, ''); const MOCK_RELEASE_TAG = `v${CRANE_VERSION}`; const MOCK_RELEASE_URL = `https://github.com/google/go-containerregistry/releases/tag/${MOCK_RELEASE_TAG}`; const MOCK_DOWNLOAD_URL_BASE = `https://github.com/google/go-containerregistry/releases/download/${MOCK_RELEASE_TAG}`; // Match the currently observed upstream naming style const MOCK_LINUX_ASSET_NAME = 'go-containerregistry_Linux_x86_64.tar.gz'; const MOCK_DARWIN_ARM64_ASSET_NAME = 'go-containerregistry_Darwin_arm64.tar.gz'; const MOCK_WINDOWS_ASSET_NAME = 'go-containerregistry_Windows_x86_64.tar.gz'; const MOCK_LINUX_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_LINUX_ASSET_NAME}`; const MOCK_DARWIN_ARM64_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_DARWIN_ARM64_ASSET_NAME}`; const MOCK_WINDOWS_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_WINDOWS_ASSET_NAME}`; const MOCK_CHECKSUM = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const MOCK_CHECKSUM_WITH_PREFIX = `sha256:${MOCK_CHECKSUM}`; const MOCK_GITHUB_RELEASES_RESPONSE = { ok: true, json: async () => [ { tag_name: MOCK_RELEASE_TAG, html_url: MOCK_RELEASE_URL, assets: [ { name: MOCK_LINUX_ASSET_NAME, browser_download_url: MOCK_LINUX_DOWNLOAD_URL, content_type: 'application/gzip', size: 12_345, digest: MOCK_CHECKSUM_WITH_PREFIX, }, { name: MOCK_DARWIN_ARM64_ASSET_NAME, browser_download_url: MOCK_DARWIN_ARM64_DOWNLOAD_URL, content_type: 'application/gzip', size: 12_345, digest: MOCK_CHECKSUM_WITH_PREFIX, }, { name: MOCK_WINDOWS_ASSET_NAME, browser_download_url: MOCK_WINDOWS_DOWNLOAD_URL, content_type: 'application/gzip', size: 12_345, digest: MOCK_CHECKSUM_WITH_PREFIX, }, ], }, ], }; const MOCK_GITHUB_ERROR_RESPONSE = { ok: false, status: 404, }; const MOCK_GITHUB_EMPTY_RELEASES = { ok: true, json: async () => [], }; const MOCK_GITHUB_RELEASES_NO_MATCHING_ASSET = { ok: true, json: async () => [ { tag_name: MOCK_RELEASE_TAG, html_url: MOCK_RELEASE_URL, assets: [ { name: 'some-other-asset.tar.gz', browser_download_url: `${MOCK_DOWNLOAD_URL_BASE}/some-other-asset.tar.gz`, content_type: 'application/gzip', size: 12_345, digest: MOCK_CHECKSUM_WITH_PREFIX, }, ], }, ], }; describe('CraneDependencyManager', () => { const originalPlatform = platform; const originalInstallationDirectory = container.resolve(InjectTokens.CraneInstallationDirectory); const temporaryDirectory = PathEx.join(getTemporaryDirectory(), 'bin'); let sandbox; before(() => { fs.mkdirSync(temporaryDirectory, { recursive: true }); sandbox = sinon.createSandbox(); }); after(() => { if (fs.existsSync(temporaryDirectory)) { fs.rmSync(temporaryDirectory, { recursive: true }); } container.register(InjectTokens.CraneInstallationDirectory, { useValue: originalInstallationDirectory }); }); afterEach(() => { container.register(InjectTokens.OsPlatform, { useValue: originalPlatform }); container.register(InjectTokens.CraneInstallationDirectory, { useValue: originalInstallationDirectory }); sandbox.restore(); }); it('should return crane version', () => { const craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, undefined, undefined); expect(craneDependencyManager.getRequiredVersion()).to.equal(version.CRANE_VERSION); }); it('should be able to check when crane not installed', () => { const craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, undefined, undefined); expect(craneDependencyManager.isInstalledLocally()).not.to.be.ok; }); it('should be able to check when crane is installed', () => { const craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, undefined, undefined); fs.writeFileSync(PathEx.join(temporaryDirectory, constants.CRANE), ''); expect(craneDependencyManager.isInstalledLocally()).to.be.ok; }); describe('CraneDependencyManager system methods', () => { let craneDependencyManager; let fetchStub; let originalFetch; beforeEach(() => { craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, process.arch, undefined); originalFetch = globalThis.fetch; globalThis.fetch = sandbox.stub(); fetchStub = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; container.register(InjectTokens.OsPlatform, { useValue: originalPlatform }); sandbox.restore(); }); it('getVersion should return version from crane version output', async () => { const executableWithPath = '/usr/local/bin/crane'; sandbox.stub(ShellRunner.prototype, 'run').withArgs(`"${executableWithPath}" version`).resolves(['0.21.4']); const actualVersion = await craneDependencyManager.getVersion(executableWithPath); expect(actualVersion).to.equal('0.21.4'); }); it('getVersion should throw error when command fails', async () => { sandbox.stub(ShellRunner.prototype, 'run').rejects(new Error('Command failed')); try { await craneDependencyManager.getVersion('/usr/local/bin/crane'); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('Failed to check crane version'); } }); it('getVersion should throw error when version pattern not found', async () => { sandbox.stub(ShellRunner.prototype, 'run').resolves(['invalid output']); try { await craneDependencyManager.getVersion('/usr/local/bin/crane'); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('Failed to check crane version'); } }); it('getArch should normalize architecture names', () => { let manager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, 'x64', undefined); // @ts-expect-error TS2341: Property getArch is protected expect(manager.getArch()).to.equal('amd64'); manager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, 'arm64', undefined); // @ts-expect-error TS2341: Property getArch is protected expect(manager.getArch()).to.equal('arm64'); manager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, 'aarch64', undefined); // @ts-expect-error TS2341: Property getArch is protected expect(manager.getArch()).to.equal('arm64'); }); it('fetchReleaseInfo should parse GitHub API response correctly for linux', async () => { fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE); container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_LINUX }); craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, 'x64', MOCK_RELEASE_TAG); // @ts-expect-error TS2341: Property fetchReleaseInfo is private const releaseInfo = await craneDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG); expect(releaseInfo.downloadUrl).to.equal(MOCK_DOWNLOAD_URL_BASE); expect(releaseInfo.assetName).to.equal(MOCK_LINUX_ASSET_NAME); expect(releaseInfo.checksum).to.equal(MOCK_CHECKSUM); expect(releaseInfo.version).to.equal(CRANE_VERSION); }); it('fetchReleaseInfo should parse GitHub API response correctly for darwin arm64', async () => { fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE); container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_DARWIN }); craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, 'arm64', MOCK_RELEASE_TAG); // @ts-expect-error TS2341: Property fetchReleaseInfo is private const releaseInfo = await craneDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG); expect(releaseInfo.assetName).to.equal(MOCK_DARWIN_ARM64_ASSET_NAME); expect(releaseInfo.checksum).to.equal(MOCK_CHECKSUM); }); it('fetchReleaseInfo should handle API error', async () => { fetchStub.resolves(MOCK_GITHUB_ERROR_RESPONSE); try { // @ts-expect-error TS2341: Property fetchReleaseInfo is private await craneDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('GitHub API request failed with status 404'); } }); it('fetchReleaseInfo should handle empty releases array', async () => { fetchStub.resolves(MOCK_GITHUB_EMPTY_RELEASES); try { // @ts-expect-error TS2341: Property fetchReleaseInfo is private await craneDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('No releases found'); } }); it('fetchReleaseInfo should handle no matching asset', async () => { fetchStub.resolves(MOCK_GITHUB_RELEASES_NO_MATCHING_ASSET); container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_LINUX }); try { // @ts-expect-error TS2341: Property fetchReleaseInfo is private await craneDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('No matching crane asset found for'); } }); }); describe('when crane is installed globally', () => { let craneDependencyManager; let runStub; let existsSyncStub; let fetchStub; let originalFetch; let packageDownloader; beforeEach(() => { packageDownloader = container.resolve(InjectTokens.PackageDownloader); craneDependencyManager = new CraneDependencyManager(undefined, undefined, temporaryDirectory, process.arch, undefined); craneDependencyManager.uninstallLocal(); runStub = sandbox.stub(craneDependencyManager, 'run'); originalFetch = globalThis.fetch; globalThis.fetch = sandbox.stub(); fetchStub = globalThis.fetch; fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE); sandbox.stub(fs, 'cpSync').returns(); sandbox.stub(fs, 'chmodSync').returns(); existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); sandbox.stub(fs, 'rmSync').returns(); }); afterEach(() => { globalThis.fetch = originalFetch; sandbox.restore(); }); it('should install crane locally if the global installation does not meet the requirements', async () => { runStub.withArgs('which crane').resolves(['/usr/local/bin/crane']); runStub.withArgs('"/usr/local/bin/crane" version').resolves([`0.1.0`]); runStub.withArgs(`"${PathEx.join(temporaryDirectory, 'crane')}" version`).resolves([`0.1.0`]); existsSyncStub.withArgs(PathEx.join(temporaryDirectory, 'crane')).returns(true); const dummyDownloadedArchive = PathEx.join(getTemporaryDirectory(), 'crane.tar.gz'); fs.writeFileSync(dummyDownloadedArchive, 'dummy'); sandbox.stub(packageDownloader, 'fetchPackage').resolves(dummyDownloadedArchive); sandbox .stub(CraneDependencyManager.prototype, 'processDownloadedPackage') .callsFake(async (_packageFilePath, temporaryDirectory) => { const executablePath = PathEx.join(temporaryDirectory, constants.CRANE); fs.mkdirSync(temporaryDirectory, { recursive: true }); fs.writeFileSync(executablePath, 'dummy executable'); return [executablePath]; }); expect(await craneDependencyManager.install(getTestCacheDirectory())).to.be.true; expect(fs.existsSync(PathEx.join(temporaryDirectory, constants.CRANE))).to.be.ok; expect(await craneDependencyManager.getExecutable()).to.equal(constants.CRANE); }); }); describe('Crane Installation Tests', () => { let originalFetch; let fetchStub; let packageDownloader; beforeEach(() => { originalFetch = globalThis.fetch; globalThis.fetch = sandbox.stub(); fetchStub = globalThis.fetch; fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE); packageDownloader = container.resolve(InjectTokens.PackageDownloader); }); afterEach(() => { globalThis.fetch = originalFetch; container.register(InjectTokens.OsPlatform, { useValue: originalPlatform }); container.register(InjectTokens.CraneInstallationDirectory, { useValue: originalInstallationDirectory }); sandbox.restore(); }); each([ [OperatingSystem.OS_LINUX, 'x64'], [OperatingSystem.OS_LINUX, 'amd64'], [OperatingSystem.OS_DARWIN, 'arm64'], ]).it('should be able to install crane base on %s and %s', async (osPlatform, osArch) => { const installationDirectory = getTemporaryDirectory(); container.register(InjectTokens.OsPlatform, { useValue: osPlatform }); container.register(InjectTokens.CraneInstallationDirectory, { useValue: installationDirectory }); const craneDependencyManager = new CraneDependencyManager(undefined, undefined, installationDirectory, osArch, MOCK_RELEASE_TAG); const dummyDownloadedArchive = PathEx.join(getTemporaryDirectory(), 'crane-package.tar.gz'); fs.writeFileSync(dummyDownloadedArchive, 'dummy'); sandbox.stub(packageDownloader, 'fetchPackage').resolves(dummyDownloadedArchive); sandbox .stub(CraneDependencyManager.prototype, 'processDownloadedPackage') .callsFake(async (_packageFilePath, temporaryDirectory) => { const executablePath = PathEx.join(temporaryDirectory, constants.CRANE); fs.mkdirSync(temporaryDirectory, { recursive: true }); fs.writeFileSync(executablePath, 'dummy executable'); return [executablePath]; }); sandbox.stub(ShellRunner.prototype, 'run').withArgs(`which ${constants.CRANE}`).alwaysReturned(false); craneDependencyManager.uninstallLocal(); expect(craneDependencyManager.isInstalledLocally()).not.to.be.ok; expect(await craneDependencyManager.install(getTestCacheDirectory())).to.be.true; expect(craneDependencyManager.isInstalledLocally()).to.be.ok; fs.rmSync(installationDirectory, { recursive: true, force: true }); }); }); }); //# sourceMappingURL=crane-dependency-manager.test.js.map