@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
321 lines • 17.3 kB
JavaScript
// 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