@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
324 lines • 16.5 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { after, before, beforeEach, afterEach, describe, it } from 'mocha';
import fs from 'node:fs';
import path from 'node:path';
import sinon from 'sinon';
import { PodmanDependencyManager } from '../../../../../src/core/dependency-managers/index.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 { container } from 'tsyringe-neo';
import { platform } from 'node:process';
import { ShellRunner } from '../../../../../src/core/shell-runner.js';
// Test data constants
const PODMAN_VERSION = version.PODMAN_VERSION.replace('v', '');
const PODMAN_LOW_VERSION = '0.1.0';
const MOCK_RELEASE_TAG = `v${PODMAN_VERSION}`;
const MOCK_RELEASE_URL = `https://github.com/containers/podman/releases/tag/${MOCK_RELEASE_TAG}`;
const MOCK_DOWNLOAD_URL_BASE = `https://github.com/containers/podman/releases/download/${MOCK_RELEASE_TAG}`;
const MOCK_LINUX_ASSET_NAME = 'podman-remote-static-linux_amd64.tar.gz';
const MOCK_WINDOWS_ASSET_NAME = 'podman-remote-release-windows_amd64.zip';
const MOCK_DARWIN_ARM64_ASSET_NAME = 'podman-remote-release-darwin_arm64.zip';
const MOCK_LINUX_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_LINUX_ASSET_NAME}`;
const MOCK_WINDOWS_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_WINDOWS_ASSET_NAME}`;
const MOCK_DARWIN_ARM64_DOWNLOAD_URL = `${MOCK_DOWNLOAD_URL_BASE}/${MOCK_DARWIN_ARM64_ASSET_NAME}`;
const MOCK_CHECKSUM = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const MOCK_CHECKSUM_WITH_PREFIX = `sha256:${MOCK_CHECKSUM}`;
// Mock GitHub API response for fetchReleaseInfo
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_WINDOWS_ASSET_NAME,
browser_download_url: MOCK_WINDOWS_DOWNLOAD_URL,
content_type: 'application/zip',
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/zip',
size: 12_345,
digest: MOCK_CHECKSUM_WITH_PREFIX,
},
],
},
],
};
// Mock GitHub API response with no matching assets
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,
},
],
},
],
};
// Mock GitHub API error response
const MOCK_GITHUB_ERROR_RESPONSE = {
ok: false,
status: 404,
};
// Mock GitHub API empty releases response
const MOCK_GITHUB_EMPTY_RELEASES = {
ok: true,
json: async () => [],
};
describe('PodmanDependencyManager', () => {
const temporaryDirectory = PathEx.join(getTemporaryDirectory(), 'bin');
const originalPlatform = platform;
let sandbox;
before(() => {
fs.mkdirSync(temporaryDirectory, { recursive: true });
sandbox = sinon.createSandbox();
});
after(() => {
if (fs.existsSync(temporaryDirectory)) {
fs.rmSync(temporaryDirectory, { recursive: true });
}
});
afterEach(() => {
sandbox.restore();
});
it('should return podman version', () => {
const podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, undefined, undefined, undefined, undefined);
expect(podmanDependencyManager.getRequiredVersion()).to.equal(version.PODMAN_VERSION);
});
it('should be able to check when podman not installed', () => {
const podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, undefined, undefined, undefined, undefined);
expect(podmanDependencyManager.isInstalledLocally()).not.to.be.ok;
});
it('should be able to check when podman is installed', async () => {
const podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, undefined, undefined, undefined, undefined);
fs.writeFileSync(PathEx.join(temporaryDirectory, constants.PODMAN), '');
expect(podmanDependencyManager.isInstalledLocally()).to.be.ok;
});
describe('PodmanDependencyManager system methods', () => {
let podmanDependencyManager;
let fetchStub;
let originalFetch;
beforeEach(() => {
podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, process.arch, undefined, undefined, undefined);
// Mock fetch for fetchReleaseInfo
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 podman --version output', async () => {
const executableWithPath = '/usr/local/bin/podman';
sandbox
.stub(ShellRunner.prototype, 'run')
.withArgs(`"${executableWithPath}" --version`)
.resolves([`podman version ${PODMAN_VERSION}`]);
const version = await podmanDependencyManager.getVersion(executableWithPath);
expect(version).to.equal(PODMAN_VERSION);
});
it('getVersion should throw error when command fails', async () => {
sandbox.stub(ShellRunner.prototype, 'run').rejects(new Error('Command failed'));
try {
await podmanDependencyManager.getVersion('/usr/local/bin/podman');
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error.message).to.include('Failed to check podman version');
}
});
it('getVersion should throw error when version pattern not found', async () => {
sandbox.stub(ShellRunner.prototype, 'run').resolves(['Invalid output']);
try {
await podmanDependencyManager.getVersion('/usr/local/bin/podman');
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error.message).to.include('Failed to check podman version');
}
});
it('shouldInstall should return false when Docker is installed', async () => {
sandbox
.stub(ShellRunner.prototype, 'run')
.withArgs(`"${constants.DOCKER}" --version`)
.resolves(['Docker version 20.10.8']);
const result = await podmanDependencyManager.shouldInstall();
expect(result).to.be.false;
});
it('shouldInstall should return true when Docker is not installed', async () => {
sandbox
.stub(ShellRunner.prototype, 'run')
.withArgs(`"${constants.DOCKER}" --version`)
.rejects(new Error('Docker not found'));
const result = await podmanDependencyManager.shouldInstall();
expect(result).to.be.true;
});
it('getArch should normalize architecture names', () => {
// Test x64 to amd64 conversion
let manager = new PodmanDependencyManager(undefined, temporaryDirectory, 'x64', undefined, undefined, undefined);
// @ts-expect-error TS2341: Property getArch is protected
expect(manager.getArch()).to.equal('amd64');
// Test arm64 conversion
manager = new PodmanDependencyManager(undefined, temporaryDirectory, 'arm64', undefined, undefined, undefined);
// @ts-expect-error TS2341: Property getArch is protected
expect(manager.getArch()).to.equal('arm64');
// Test aarch64 to arm64 conversion
manager = new PodmanDependencyManager(undefined, temporaryDirectory, 'aarch64', undefined, undefined, undefined);
// @ts-expect-error TS2341: Property getArch is protected
expect(manager.getArch()).to.equal('arm64');
});
it('fetchReleaseInfo should parse GitHub API response correctly', async () => {
fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE);
container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_LINUX });
podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, 'x64', undefined, undefined, undefined);
// @ts-expect-error TS2341: Property fetchReleaseInfo is private
const releaseInfo = await podmanDependencyManager.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(PODMAN_VERSION);
});
it('fetchReleaseInfo should handle API error', async () => {
fetchStub.resolves(MOCK_GITHUB_ERROR_RESPONSE);
try {
// @ts-expect-error TS2341: Property fetchReleaseInfo is private
await podmanDependencyManager.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 podmanDependencyManager.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);
try {
// @ts-expect-error TS2341: Property fetchReleaseInfo is private
await podmanDependencyManager.fetchReleaseInfo(MOCK_RELEASE_TAG);
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error.message).to.include('No matching asset found for');
}
});
});
describe('when podman is installed globally', () => {
let podmanDependencyManager;
let runStub;
let existsSyncStub;
let fetchStub;
let originalFetch;
beforeEach(() => {
podmanDependencyManager = new PodmanDependencyManager(undefined, temporaryDirectory, process.arch, undefined, undefined, undefined);
podmanDependencyManager.uninstallLocal();
runStub = sandbox.stub(podmanDependencyManager, 'run');
// Mock fetch for fetchReleaseInfo
originalFetch = globalThis.fetch;
globalThis.fetch = sandbox.stub();
fetchStub = globalThis.fetch;
// Configure fetch to return valid mock response
fetchStub.resolves(MOCK_GITHUB_RELEASES_RESPONSE);
// Add stubs for file system operations
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 prefer the global installation if it meets the requirements', async () => {
sandbox.stub(podmanDependencyManager, 'shouldInstall').resolves(true);
const fakeGlobalBinDirectory = '/test-solo-global-bin';
const fakeGlobalPodmanPath = `${fakeGlobalBinDirectory}/podman`;
const originalPath = process.env.PATH ?? '';
process.env.PATH = `${fakeGlobalBinDirectory}${path.delimiter}${originalPath}`;
sandbox.stub(fs, 'accessSync').callsFake((filePath) => {
if (String(filePath) === fakeGlobalPodmanPath) {
return;
}
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});
runStub.withArgs(`"${fakeGlobalPodmanPath}" --version`).resolves([`podman version ${version.PODMAN_VERSION}`]);
existsSyncStub.withArgs(`${temporaryDirectory}/podman`).returns(false);
try {
// @ts-expect-error TS2341: Property isInstalledGloballyAndMeetsRequirements is private
const result = await podmanDependencyManager.isInstalledGloballyAndMeetsRequirements();
expect(result).to.be.true;
expect(await podmanDependencyManager.install(getTestCacheDirectory())).to.be.true;
// Should return global path since it meets requirements
expect(await podmanDependencyManager.getExecutable()).to.equal(constants.PODMAN);
}
finally {
process.env.PATH = originalPath;
}
});
it('should install podman locally if the global installation does not meet the requirements', async () => {
const fakeGlobalBinDirectory = '/test-solo-global-bin';
const fakeGlobalPodmanPath = `${fakeGlobalBinDirectory}/podman`;
const originalPath = process.env.PATH ?? '';
process.env.PATH = `${fakeGlobalBinDirectory}${path.delimiter}${originalPath}`;
sandbox.stub(fs, 'accessSync').callsFake((filePath) => {
if (String(filePath) === fakeGlobalPodmanPath) {
return;
}
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});
runStub.withArgs(`"${fakeGlobalPodmanPath}" --version`).resolves([`podman version ${PODMAN_LOW_VERSION}`]);
runStub
.withArgs(`"${PathEx.join(temporaryDirectory, 'podman')}" --version`)
.resolves([`podman version ${PODMAN_LOW_VERSION}`]);
existsSyncStub.withArgs(PathEx.join(temporaryDirectory, 'podman')).returns(true);
try {
// @ts-expect-error TS2341: Property isInstalledGloballyAndMeetsRequirements is private
const result = await podmanDependencyManager.isInstalledGloballyAndMeetsRequirements();
expect(result).to.be.false;
expect(await podmanDependencyManager.install(getTestCacheDirectory())).to.be.true;
expect(fs.existsSync(PathEx.join(temporaryDirectory, 'podman'))).to.be.ok;
expect(await podmanDependencyManager.getExecutable()).to.equal(constants.PODMAN);
}
finally {
process.env.PATH = originalPath;
}
});
});
});
//# sourceMappingURL=podman-dependency-manager.test.js.map