@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
245 lines • 13.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'mocha';
import sinon from 'sinon';
import fs from 'node:fs';
import * as Base64 from 'js-base64';
import { getMirrorNodeReleaseTag, PostgresSharedResource } from '../../../../src/core/shared-resources/postgres.js';
import { NamespaceName } from '../../../../src/types/namespace/namespace-name.js';
import { SoloError } from '../../../../src/core/errors/solo-error.js';
import * as constants from '../../../../src/core/constants.js';
import { PodReference } from '../../../../src/integration/kube/resources/pod/pod-reference.js';
import { PodName } from '../../../../src/integration/kube/resources/pod/pod-name.js';
describe('PostgresSharedResource', () => {
const encode = (s) => Base64.encode(s);
const namespace = NamespaceName.of('test-namespace');
const context = 'test-context';
let loggerStub;
let helmStub;
let chartManagerStub;
let k8FactoryStub;
let podsStub;
let containersStub;
let secretsStub;
let k8Stub;
let k8ContainerStub;
let postgres;
const postgresPodReference = PodReference.of(namespace, PodName.of('solo-shared-resources-postgres-0'));
beforeEach(() => {
helmStub = sinon.stub();
chartManagerStub = sinon.stub();
loggerStub = sinon.stub();
loggerStub.info = sinon.stub();
loggerStub.error = sinon.stub();
k8ContainerStub = {
copyTo: sinon.stub().resolves(),
execContainer: sinon.stub().resolves(),
};
podsStub = {
waitForRunningPhase: sinon.stub().resolves(),
list: sinon.stub().resolves([{ podReference: postgresPodReference }]),
};
secretsStub = {
list: sinon.stub().resolves([]),
read: sinon.stub().resolves({}),
};
containersStub = {
readByRef: sinon.stub().returns(k8ContainerStub),
};
k8Stub = {
pods: sinon.stub().returns(podsStub),
containers: sinon.stub().returns(containersStub),
secrets: sinon.stub().returns(secretsStub),
};
k8FactoryStub = sinon.stub();
k8FactoryStub.getK8 = sinon.stub().returns(k8Stub);
postgres = new PostgresSharedResource(loggerStub, k8FactoryStub, helmStub, chartManagerStub);
});
afterEach(() => {
sinon.restore();
});
describe('waitForPodReady()', () => {
it('calls waitForRunningPhase with postgres labels', async () => {
await postgres.waitForPodReady(namespace, context);
expect(k8FactoryStub.getK8).to.have.been.calledWith(context);
expect(podsStub.waitForRunningPhase).to.have.been.calledOnce;
const [, labels] = podsStub.waitForRunningPhase.firstCall.args;
expect(labels).to.include('app.kubernetes.io/name=postgres');
expect(labels).to.include('app.kubernetes.io/instance=solo-shared-resources');
});
it('passes the namespace and configured constants for max attempts and delay', async () => {
await postgres.waitForPodReady(namespace, context);
const [passedNamespace, , maxAttempts, delay] = podsStub.waitForRunningPhase.firstCall.args;
expect(passedNamespace).to.equal(namespace);
expect(maxAttempts).to.equal(constants.PODS_RUNNING_MAX_ATTEMPTS);
expect(delay).to.equal(constants.PODS_RUNNING_DELAY);
});
});
describe('initializeMirrorNode()', () => {
const postgresPasswordsSecret = {
name: 'solo-shared-resources-passwords',
data: { password: encode('superpassword') },
};
const mirrorPasswordsSecret = {
data: {
HIERO_MIRROR_IMPORTER_DB_NAME: encode('mirror_node'),
HIERO_MIRROR_IMPORTER_DB_OWNER: encode('mirror_node_owner'),
HIERO_MIRROR_IMPORTER_DB_OWNERPASSWORD: encode('ownerpass'),
HIERO_MIRROR_GRAPHQL_DB_PASSWORD: encode('graphqlpass'),
HIERO_MIRROR_GRPC_DB_PASSWORD: encode('grpcpass'),
HIERO_MIRROR_IMPORTER_DB_PASSWORD: encode('importerpass'),
HIERO_MIRROR_REST_DB_PASSWORD: encode('restpass'),
HIERO_MIRROR_RESTJAVA_DB_PASSWORD: encode('restjavapass'),
HIERO_MIRROR_ROSETTA_DB_PASSWORD: encode('rosettapass'),
HIERO_MIRROR_WEB3_DB_PASSWORD: encode('web3pass'),
},
};
let existsSyncStub;
let mkdirSyncStub;
let writeFileSyncStub;
let _rmSyncStub;
beforeEach(() => {
secretsStub.list.resolves([postgresPasswordsSecret]);
secretsStub.read.resolves(mirrorPasswordsSecret);
// Simulate init script already cached so we skip the download path
existsSyncStub = sinon.stub(fs, 'existsSync').returns(true);
mkdirSyncStub = sinon.stub(fs, 'mkdirSync');
writeFileSyncStub = sinon.stub(fs, 'writeFileSync');
_rmSyncStub = sinon.stub(fs, 'rmSync');
});
it('reads secrets from correct labels and secret names', async () => {
await postgres.initializeMirrorNode(namespace, context);
expect(secretsStub.list).to.have.been.calledWith(namespace, ['app.kubernetes.io/instance=solo-shared-resources']);
expect(secretsStub.read).to.have.been.calledWith(namespace, 'mirror-passwords');
});
it('copies check script, init script, and wrapper script to the postgres pod', async () => {
await postgres.initializeMirrorNode(namespace, context);
expect(k8ContainerStub.copyTo).to.have.been.calledTwice;
for (const call of k8ContainerStub.copyTo.getCalls()) {
expect(call.args[1]).to.equal('/tmp');
}
});
it('executes the wrapper script inside the container', async () => {
await postgres.initializeMirrorNode(namespace, context);
const execCalls = k8ContainerStub.execContainer.args.map((a) => a[0]);
expect(execCalls.some((c) => c.includes('/bin/bash /tmp/run-init.sh'))).to.be.true;
});
it('wrapper script contains correct DB_NAME and OWNER_USERNAME from secrets', async () => {
await postgres.initializeMirrorNode(namespace, context);
const wrapperArguments = writeFileSyncStub
.getCalls()
.find((call) => call.args[0].includes('run-init')).args;
const writtenContent = wrapperArguments[1];
expect(writtenContent).to.include('export DB_NAME=mirror_node');
expect(writtenContent).to.include('export OWNER_USERNAME=mirror_node_owner');
});
it('wrapper script contains all required service passwords', async () => {
await postgres.initializeMirrorNode(namespace, context);
const wrapperArguments = writeFileSyncStub
.getCalls()
.find((call) => call.args[0].includes('run-init')).args;
const writtenContent = wrapperArguments[1];
expect(writtenContent).to.include('export GRAPHQL_PASSWORD=graphqlpass');
expect(writtenContent).to.include('export GRPC_PASSWORD=grpcpass');
expect(writtenContent).to.include('export IMPORTER_PASSWORD=importerpass');
expect(writtenContent).to.include('export REST_PASSWORD=restpass');
expect(writtenContent).to.include('export REST_JAVA_PASSWORD=restjavapass');
expect(writtenContent).to.include('export ROSETTA_PASSWORD=rosettapass');
expect(writtenContent).to.include('export WEB3_PASSWORD=web3pass');
});
it('uses a custom prefix when provided', async () => {
const customMirrorPasswordsSecret = {
data: {
CUSTOM_MIRROR_IMPORTER_DB_NAME: encode('custom_db'),
CUSTOM_MIRROR_IMPORTER_DB_OWNER: encode('custom_owner'),
CUSTOM_MIRROR_IMPORTER_DB_OWNERPASSWORD: encode('custom_ownerpass'),
CUSTOM_MIRROR_GRAPHQL_DB_PASSWORD: encode('graphqlpass'),
CUSTOM_MIRROR_GRPC_DB_PASSWORD: encode('grpcpass'),
CUSTOM_MIRROR_IMPORTER_DB_PASSWORD: encode('importerpass'),
CUSTOM_MIRROR_REST_DB_PASSWORD: encode('restpass'),
CUSTOM_MIRROR_RESTJAVA_DB_PASSWORD: encode('restjavapass'),
CUSTOM_MIRROR_ROSETTA_DB_PASSWORD: encode('rosettapass'),
CUSTOM_MIRROR_WEB3_DB_PASSWORD: encode('web3pass'),
},
};
secretsStub.read.resolves(customMirrorPasswordsSecret);
await postgres.initializeMirrorNode(namespace, context, 'CUSTOM');
const wrapperArguments = writeFileSyncStub
.getCalls()
.find((call) => call.args[0].includes('run-init')).args;
const writtenContent = wrapperArguments[1];
expect(writtenContent).to.include('export DB_NAME=custom_db');
expect(writtenContent).to.include('export OWNER_USERNAME=custom_owner');
});
it('uses the postgres pod from namespace zero', async () => {
await postgres.initializeMirrorNode(namespace, context);
const containerReference = containersStub.readByRef.firstCall.args[0];
expect(containerReference.parentReference.name.name).to.equal(postgresPodReference.name.name);
});
it('throws SoloError when container copy fails', async () => {
k8ContainerStub.copyTo.rejects(new Error('copy failed'));
await expect(postgres.initializeMirrorNode(namespace, context)).to.be.rejectedWith(SoloError, 'Failed to copy Mirror Node Postgres initialization script to container');
});
it('retries execution and throws SoloError after max attempts', async () => {
// chmod calls must succeed (they are in the outer try-catch that throws immediately).
// Only the actual bash script execution should fail to exercise the retry loop.
k8ContainerStub.execContainer.callsFake((cmd) => {
if (cmd.startsWith('chmod')) {
return Promise.resolve();
}
return Promise.reject(new Error('exec failed'));
});
await expect(postgres.initializeMirrorNode(namespace, context)).to.be.rejectedWith(SoloError);
expect(loggerStub.error).to.have.been.called;
});
it('skips downloading init script when already cached', async () => {
// existsSync returns true — simulating cached file
const fetchStub = sinon.stub(globalThis, 'fetch');
await postgres.initializeMirrorNode(namespace, context);
expect(fetchStub).to.not.have.been.called;
});
it('creates cache directory and downloads init script when not cached', async () => {
const fakeStream = {
write: sinon.stub().callsArg(1),
end: sinon.stub(),
on: sinon.stub(),
};
sinon.stub(fs, 'createWriteStream').returns(fakeStream);
existsSyncStub.restore();
// First call (directory check) returns false, second (file check) returns false
existsSyncStub = sinon.stub(fs, 'existsSync');
existsSyncStub.onFirstCall().returns(false);
existsSyncStub.onSecondCall().returns(false);
const mockResponse = {
ok: true,
body: undefined,
arrayBuffer: sinon.stub().resolves(Buffer.from('#!/bin/bash\necho ok').buffer),
};
const fetchStub = sinon.stub(globalThis, 'fetch').resolves(mockResponse);
await postgres.initializeMirrorNode(namespace, context);
expect(mkdirSyncStub).to.have.been.calledOnce;
expect(fetchStub).to.have.been.calledOnce;
const fetchUrl = fetchStub.firstCall.args[0];
expect(fetchUrl).to.include('hiero-mirror-node');
expect(fetchUrl).to.include('init.sh');
});
it('throws when init script download fails', async () => {
existsSyncStub.restore();
existsSyncStub = sinon.stub(fs, 'existsSync');
existsSyncStub.onFirstCall().returns(false);
existsSyncStub.onSecondCall().returns(false);
const mockResponse = { ok: false, status: 404, statusText: 'Not Found', body: undefined };
sinon.stub(globalThis, 'fetch').resolves(mockResponse);
await expect(postgres.initializeMirrorNode(namespace, context)).to.be.rejectedWith('Failed to download Mirror Node Postgres init script');
});
});
describe('getMirrorNodeReleaseTag()', () => {
it('preserves pre-release suffixes', () => {
expect(getMirrorNodeReleaseTag('v0.153.0-rc2')).to.equal('v0.153.0-rc2');
});
it('adds v prefix when missing', () => {
expect(getMirrorNodeReleaseTag('0.153.0-rc2')).to.equal('v0.153.0-rc2');
});
});
});
//# sourceMappingURL=postgres.test.js.map