UNPKG

@hashgraph/solo

Version:

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

245 lines 13.3 kB
// 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