Skip to content
Open
53 changes: 53 additions & 0 deletions .github/actions/setup/check-package-lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Check Package Lock File

permissions:
contents: read

concurrency:
group: check-package-lock-${{ github.ref }}
cancel-in-progress: true

on:
push:
branches:
- main # Run on push to main branch only
pull_request:
branches:
- "**" # Run on PR to any branch

jobs:
verify-package-lock:
name: Verify package-lock.json exists
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check if package-lock.json exists
run: |
if [ ! -f "package-lock.json" ]; then
echo "ERROR: package-lock.json file is missing from the repository"
echo "This file is required to ensure consistent dependency versions across all environments"
echo "Please ensure package-lock.json is committed with your changes"
exit 1
fi
echo "SUCCESS: package-lock.json file is present"

- name: Verify package-lock.json is not empty
run: |
if [ ! -s "package-lock.json" ]; then
echo "ERROR: package-lock.json file exists but is empty"
echo "Please run 'npm install' to regenerate the lock file"
exit 1
fi
echo "SUCCESS: package-lock.json file is valid and not empty"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Validate package-lock.json is valid and in sync
run: npm ci --dry-run --ignore-scripts
5 changes: 5 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jobs:
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand Down
6 changes: 6 additions & 0 deletions src/modules/http-client/http-client-module-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ class HttpClientModuleManager extends BaseModuleManager {
return this.getImplementation().module.initializeAfterMiddlewares(this.authService);
}
}

async close() {
if (this.initialized && this.getImplementation().module.close) {
return this.getImplementation().module.close();
}
}
}

export default HttpClientModuleManager;
21 changes: 20 additions & 1 deletion src/modules/http-client/implementation/express-http-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,30 @@ class ExpressHttpClient {
);
this.httpsServer.listen(this.config.port);
} else {
this.app.listen(this.config.port);
this.server = this.app.listen(this.config.port);
}
this.logger.info(`Node listening on port: ${this.config.port}`);
}

async close() {
return new Promise((resolve, reject) => {
const serverToClose = this.httpsServer || this.server;
if (serverToClose) {
serverToClose.close((err) => {
if (err) {
this.logger.error(`Error closing HTTP server: ${err.message}`);
reject(err);
} else {
this.logger.info('HTTP server closed successfully');
resolve();
}
});
} else {
resolve();
}
});
}

selectMiddlewares(options) {
const middlewares = [];
if (options.rateLimit) middlewares.push(rateLimiterMiddleware(this.config.rateLimiter));
Expand Down
79 changes: 56 additions & 23 deletions test/bdd/steps/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,7 @@ Given(
if (response.error) {
assert.fail(`Error while initializing node${nodeIndex}: ${response.error}`);
} else {
// todo if started
const client = new DkgClientHelper({
endpoint: 'http://localhost',
port: rpcPort,
maxNumberOfRetries: 5,
frequency: 2,
contentType: 'all',
});
// Build blockchain options BEFORE creating the client
let clientBlockchainOptions = {};
Object.keys(this.state.localBlockchains).forEach((blockchainId, index) => {
const blockchain = this.state.localBlockchains[blockchainId];
Expand All @@ -87,6 +80,25 @@ Given(
};
});

// Get the first blockchain for the DKG client config
const firstBlockchainId = Object.keys(this.state.localBlockchains)[0];
const firstBlockchain = this.state.localBlockchains[firstBlockchainId];
const firstWallet = firstBlockchain.getWallets()[0];

const client = new DkgClientHelper({
endpoint: 'http://localhost',
port: rpcPort,
maxNumberOfRetries: 5,
frequency: 2,
contentType: 'all',
blockchain: {
name: firstBlockchainId,
publicKey: firstWallet.address,
privateKey: firstWallet.privateKey,
rpc: `http://localhost:${firstBlockchain.port}`,
},
});

this.state.nodes[nodeIndex] = {
client,
forkedNode,
Expand Down Expand Up @@ -145,23 +157,44 @@ Given(
fs.rmSync(appDataPath, { recursive: true, force: true });

const nodeInstance = new MockOTNode(nodeConfiguration);
await nodeInstance.start(); // This will skip startNetworkModule

try {
await nodeInstance.start(); // This will skip startNetworkModule

// Get the first blockchain for the DKG client config
const firstBlockchainId = Object.keys(this.state.localBlockchains)[0];
const firstBlockchain = this.state.localBlockchains[firstBlockchainId];
const firstWallet = firstBlockchain.getWallets()[0];

const client = new DkgClientHelper({
endpoint: 'http://localhost',
port: rpcPort,
useSSL: false,
timeout: 25,
loglevel: 'trace',
});
const client = new DkgClientHelper({
endpoint: 'http://localhost',
port: rpcPort,
useSSL: false,
timeout: 25,
loglevel: 'trace',
blockchain: {
name: firstBlockchainId,
publicKey: firstWallet.address,
privateKey: firstWallet.privateKey,
rpc: `http://localhost:${firstBlockchain.port}`,
},
});

this.state.bootstraps.push({
client,
otNodeInstance: nodeInstance,
configuration: nodeConfiguration,
nodeRpcUrl: `http://localhost:${rpcPort}`,
fileService: nodeInstance.fileService,
});
this.state.bootstraps.push({
client,
otNodeInstance: nodeInstance,
configuration: nodeConfiguration,
nodeRpcUrl: `http://localhost:${rpcPort}`,
fileService: nodeInstance.fileService,
});
} catch (error) {
// Ensure node is stopped if there's an error after starting
this.logger.error(`Error during bootstrap initialization: ${error.message}`);
if (nodeInstance.stop) {
await nodeInstance.stop();
}
throw error;
}
}
);
//
Expand Down
71 changes: 46 additions & 25 deletions test/bdd/steps/hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ process.env.NODE_ENV = NODE_ENVIRONMENTS.TEST;

BeforeAll(() => {});

Before(function beforeMethod(testCase, done) {
Before(function beforeMethod(testCase) {
this.logger = console;
this.logger.log('\n🟡 Starting scenario:', testCase.pickle.name);
// Initialize variables
Expand All @@ -24,48 +24,58 @@ Before(function beforeMethod(testCase, done) {
fs.mkdirSync(logDir, { recursive: true });
this.state.scenarionLogDir = logDir;
this.logger.log('📁 Scenario logs:', logDir);
done();
});

After(async function afterMethod(testCase, done) {
After({ timeout: 30000 }, async function afterMethod(testCase) {
const tripleStoreConfiguration = [];
const databaseNames = [];
const promises = [];
const cleanupPromises = [];

// Stop all nodes first and wait for them to shut down
const stopPromises = [];

for (const key in this.state.nodes) {
const node = this.state.nodes[key];
if (node.forkedNode) {
node.forkedNode.kill();
} else if (node.otNodeInstance?.stop) {
promises.push(node.otNodeInstance.stop());
stopPromises.push(node.otNodeInstance.stop());
}

tripleStoreConfiguration.push({
modules: { tripleStore: node.configuration.modules.tripleStore },
});
databaseNames.push(node.configuration.operationalDatabase.databaseName);
const dataFolderPath = node.fileService.getDataFolderPath();
promises.push(node.fileService.removeFolder(dataFolderPath));
cleanupPromises.push(node.fileService.removeFolder(dataFolderPath));
}

for (const node of this.state.bootstraps) {
if (node.forkedNode) {
node.forkedNode.kill();
} else if (node.otNodeInstance?.stop) {
promises.push(node.otNodeInstance.stop());
stopPromises.push(node.otNodeInstance.stop());
}

tripleStoreConfiguration.push({
modules: { tripleStore: node.configuration.modules.tripleStore },
});
databaseNames.push(node.configuration.operationalDatabase.databaseName);
const dataFolderPath = node.fileService.getDataFolderPath();
promises.push(node.fileService.removeFolder(dataFolderPath));
cleanupPromises.push(node.fileService.removeFolder(dataFolderPath));
}

// Wait for all nodes to stop before continuing
this.logger.log('⏸️ Stopping all nodes...');
await Promise.all(stopPromises);
this.logger.log('✅ All nodes stopped');

// Give a moment for ports to be fully released
await new Promise(resolve => setTimeout(resolve, 500));

for (const localBlockchain in this.state.localBlockchains) {
this.logger.info(`🛑 Stopping local blockchain ${localBlockchain}`);
promises.push(this.state.localBlockchains[localBlockchain].stop());
cleanupPromises.push(this.state.localBlockchains[localBlockchain].stop());
this.state.localBlockchains[localBlockchain] = null;
}

Expand All @@ -78,38 +88,49 @@ After(async function afterMethod(testCase, done) {

for (const db of databaseNames) {
const sql = `DROP DATABASE IF EXISTS \`${db}\`;`;
promises.push(con.promise().query(sql));
cleanupPromises.push(con.promise().query(sql));
}

for (const config of tripleStoreConfiguration) {
promises.push((async () => {
const tripleStoreModuleManager = new TripleStoreModuleManager({
config,
logger: this.logger,
});
await tripleStoreModuleManager.initialize();
for (const impl of tripleStoreModuleManager.getImplementationNames()) {
const { tripleStoreConfig } = tripleStoreModuleManager.getImplementation(impl);
for (const repo of Object.keys(tripleStoreConfig.repositories)) {
this.logger.log('🗑 Removing triple store repository:', repo);
await tripleStoreModuleManager.deleteRepository(impl, repo);
// Skip if tripleStore module is not defined
if (!config?.modules?.tripleStore) {
continue;
}

cleanupPromises.push((async () => {
try {
const tripleStoreModuleManager = new TripleStoreModuleManager({
config,
logger: this.logger,
});
await tripleStoreModuleManager.initialize();
for (const impl of tripleStoreModuleManager.getImplementationNames()) {
const { tripleStoreConfig } = tripleStoreModuleManager.getImplementation(impl);
for (const repo of Object.keys(tripleStoreConfig.repositories)) {
this.logger.log('🗑 Removing triple store repository:', repo);
await tripleStoreModuleManager.deleteRepository(impl, repo);
}
}
} catch (error) {
// Log but don't fail cleanup if tripleStore cleanup fails
this.logger.warn(`⚠️ Could not clean up tripleStore: ${error.message}`);
}
})());
}

await Promise.all(promises);
await Promise.all(cleanupPromises);
con.end();

this.logger.log('\n✅ Completed scenario:', testCase.pickle.name);
this.logger.log(`📄 Location: ${testCase.gherkinDocument.uri}:${testCase.gherkinDocument.feature.location.line}`);
this.logger.log(`🟢 Status: ${testCase.result.status}`);
this.logger.log(`⏱ Duration: ${testCase.result.duration} milliseconds\n`);
done();
});

AfterAll(async () => {});

process.on('unhandledRejection', () => {
process.abort();
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Don't abort in test environment, just log the error
// process.abort();
});
28 changes: 28 additions & 0 deletions test/utilities/MockOTNode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,32 @@ export default class MockOTNode extends OTNode {
this.logger.info('[Mock] Skipping startNetworkModule in test');
// Do nothing
}

async stop() {
this.logger.info('[Mock] Stopping node...');
try {
// Stop command executor
const commandExecutor = this.container?.resolve('commandExecutor');
if (commandExecutor) {
await commandExecutor.commandExecutorShutdown();
}

// Stop HTTP server
const httpClientModuleManager = this.container?.resolve('httpClientModuleManager');
if (httpClientModuleManager?.close) {
await httpClientModuleManager.close();
}

// Stop blockchain event listeners
const blockchainEventsService = this.container?.resolve('blockchainEventsService');
if (blockchainEventsService?.stopListening) {
await blockchainEventsService.stopListening();
}

this.logger.info('[Mock] Node stopped successfully');
} catch (error) {
this.logger.error(`[Mock] Error stopping node: ${error.message}`);
throw error;
}
}
}
Loading