From aea5e35bb6d2efaedb1a44e5d0dc14d629feb600 Mon Sep 17 00:00:00 2001 From: "Victor Amaral de Sousa, PhD" Date: Sat, 14 Feb 2026 08:33:25 +0100 Subject: [PATCH 1/4] update to v1 --- .kotlin/errors/errors-1770458785068.log | 4 + README-v0.3.md | 416 ++++++++++++ README.md | 631 +++++++----------- build.gradle.kts | 15 +- src/main/kotlin/Main.kt | 17 +- .../be/vamaralds/lib/ChaincodeTestCase.kt | 17 +- .../kotlin/be/vamaralds/lib/ChaincodeTestr.kt | 12 +- .../be/vamaralds/lib/ContractHandler.kt | 34 +- .../lib/MultiOrgConnectionManager.kt | 76 +++ .../be/vamaralds/lib/PayloadResolver.kt | 75 +++ .../kotlin/be/vamaralds/lib/TestChaincode.kt | 207 +++++- .../be/vamaralds/lib/WorkspaceManager.kt | 120 ++++ src/main/resources/logging.properties | 11 + src/main/resources/simplelogger.properties | 19 + src/test/resources/multi-org-test-suite.json | 311 +++++++++ task.md | 30 + 16 files changed, 1586 insertions(+), 409 deletions(-) create mode 100644 .kotlin/errors/errors-1770458785068.log create mode 100644 README-v0.3.md create mode 100644 src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt create mode 100644 src/main/kotlin/be/vamaralds/lib/PayloadResolver.kt create mode 100644 src/main/kotlin/be/vamaralds/lib/WorkspaceManager.kt create mode 100644 src/main/resources/logging.properties create mode 100644 src/main/resources/simplelogger.properties create mode 100644 src/test/resources/multi-org-test-suite.json create mode 100644 task.md diff --git a/.kotlin/errors/errors-1770458785068.log b/.kotlin/errors/errors-1770458785068.log new file mode 100644 index 0000000..7cd151b --- /dev/null +++ b/.kotlin/errors/errors-1770458785068.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.20 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/README-v0.3.md b/README-v0.3.md new file mode 100644 index 0000000..608ebe7 --- /dev/null +++ b/README-v0.3.md @@ -0,0 +1,416 @@ +# ChaincodeTestr +[![Build](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml/badge.svg)](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml) + + +CLI application that supports the execution of a test suite on a B-MERODE chaincode, deployed on a HLF network. +The below video provides the explanation and a demonstration. The remainder of the README provides all the detailed explanations. The test suite used as demonstration has been developed for a B-MERODE model developed by Wim Laurier, and based on the MERODE model presented in the following paper: + +Laurier, W., Horiuchi, S., & Snoeck, M. (2021). An executable axiomatization of the REA2 ontology. Journal of Information Systems, 35(3), 133-154. + +[![B-MERODE Chaincode Tester Demo](https://img.youtube.com/vi/V4gcxtX9kZk/0.jpg)](https://www.youtube.com/watch?v=V4gcxtX9kZk) + + +# Running the Application +Using the terminal, go into the bin folder, and you can run the following commands: +```shell +./ChaincoderTestr --help +``` +You obtain the following output: +```shell +Usage: test-chaincode [OPTIONS] + +Options: + --wallet-path PATH Path to the wallet containing the identity + to use for the connection + --identity-name TEXT Name of the identity to use for the + connection (default: Org1 Admin) + --connection-profile-path PATH Absolute Path to the connection profile + --chaincode-name TEXT Name of the chaincode to test + --test-suite-path PATH Path to the testsuite to run + -h, --help Show this message and exit +``` +To run a test suite, you need to specify all the options above. For instance: +```shell +./ChaincoderTestr --wallet-path chaincode-test/wallets/org1 --identity-name "Org1 Admin" --connection-profile-path chaincode-test/connection-profiles/org1.json --chaincode-name rea --test-suite-path chaincode-test/suites/happy-path-test-suite.json +``` + +Assuming that the HLF network is running and that the provided details are correct, the example provides the following output: +````shell +===== Configuring Chaincode Test Suite ===== +Network and Gateway Configuration: {"identityName":"Org1 Admin","channelName":"mychannel","walletPath":"/Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/wallets/org1","connectionProfilePath":"/Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/connection-profiles/org1.json","chaincodeName":"rea"} +Test Suite Specification: /Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/suites/happy-path-test-suite.json +===== Parsing Test Suite ===== +Test Suite Successfully Parsed: 8 test cases found +===== Connection to the Network ===== +Successfully connected to the network +===== Initializing Collaboration with Sender as Participants Handler ===== +===== Running Test Suite ===== +Test Case: Create First EconomicAgent (Elmo) Successful ✅ +Test Case: Create Second EconomicAgent(Cookie) Successful ✅ +Test Case: Create Economic Resource Resource (Cookie) Successful ✅ +Test Case: Create Economic Resource Resource (Cash) Successful ✅ +Test Case: Register Ownership of Cash by Cookie Successful ✅ +Test Case: Register Ownership of Cookie by Elmo Successful ✅ +Test Case: Create Economic Event (delivery) Failed ❌: Expected transaction to succeed, but it failed (Transaction handleEvent failed: No valid proposal responses received. 2 peer error responses: Error during contract method execution; Error during contract method execution) +Test Case: Create Economic Event (payment) Failed ❌: Expected transaction to succeed, but it failed (Transaction handleEvent failed: No valid proposal responses received. 2 peer error responses: Error during contract method execution; Error during contract method execution) +===== Test Suite Results Summary ===== +Number of Tests: 8 +Number of Successful Tests: 6 +Number of Failed Tests: 2 +===== Closing Gateway Connection ===== +```` +_Note: additional logging output is generated by the HLF gateway client, they can be ignored._ + +# Extracting the Wallets and Connection Profiles +To obtain the wallet folder and the connection profile file, you can use the export functionality of the IBM Blockchain Platform extension for VSCode. + +# Preparing the Test Suite Specification +To specify the test suites, the application relies on a _.json_. + +The fields to specify are the following: +* _name_: the name of the test suite +* _testsCases_: an array of test cases to run + +Each test case is specified as follows: +* _name_: the name of the test case +* _businessEventName_: the name of the business event to fire +* _payload_: the payload of the business event (JSON) +* _expectedToSucceed_: true / false: defines whether the business event is expected to be successfully executed or not +* _thenMarkAsReady_: true / false: defines whether the B-MERODE collaboration shuold be marked as ready after the execution of the business event +* _expectedAttributeValues_: an array of tests to run on the attributes of the business event. Each test is specified as follows: + * _boId_: id of the business object whose attributes values will be tested + * _attributeName_: the name of the attribute (_starting with a lower-case, even if the business object attribute name in the chaincode starts with an upper-case_) + * _expectedValue_: expected value for the attribute +* _expectedBOStates_: an array of tests to run on the states of the business objects. Each test is specified as follows: + * _boId_: id of the business object whose state will be tested + * _expectedStateName_: expected state (name) for the business object + +Note that the _expectedAttributeValues_ and the _expectedBOStates_ are optional. If not specified, the application will not perform any test on the attributes and the states of the business objects. +Also, these fields allow specifying tests for any business object (not only the one created/modified after the execution of the business event), as long as the _boId_ is correctly specified. It is therefore possible to check the propagated effects of business events. + +````json +{ + "name": "Happy Path REA", + "testsCases": [ + { + "name": "Create First EconomicAgent (Elmo)", + "businessEventName": "EVcrEconomicAgent", + "payload": { + "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOqHVWeIlraubwG+CsmobuigvO0TeaXJbmS8QkkwTOTtBtogKFVjj9PyBt0hekRKy8wtVllQpfu+UgfrITok5BA==", + "Name": "Elmo" + }, + "expectedToSucceed": true, + "thenMarkAsReady": true, + "expectedAttributeValues": [ + { + "boId": "EconomicAgent#0", + "attributeName": "name", + "expectedValue": "Elmo" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Second EconomicAgent(Cookie)", + "businessEventName": "EVcrEconomicAgent", + "payload": { + "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMkHRF1L6deb2FCwQkZbwNeGtp00kc/sxd2R5XLfiGCM6yA9F4NJXPg1DdNZ45x2YqcDn5lBJKkzHvNMVADMBIw==", + "Name": "Cookie" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicAgent#1", + "attributeName": "name", + "expectedValue": "Cookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Economic Resource Resource (Cookie)", + "businessEventName": "EVcrEconomicResource", + "payload": { + "Name": "Cookie" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicResource#0", + "attributeName": "name", + "expectedValue": "Cookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Economic Resource Resource (Cash)", + "businessEventName": "EVcrEconomicResource", + "payload": { + "Name": "Cash" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicResource#1", + "attributeName": "name", + "expectedValue": "Cash" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Register Ownership of Cash by Cookie", + "businessEventName": "EVcrOwnership", + "payload": { + "Name": "CashOwnedByCookie", + "economicResourceId_EconomicResource_Ownership": "EconomicResource#1", + "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#1" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "Ownership#0", + "attributeName": "name", + "expectedValue": "CashOwnedByCookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + } + ] + }, + { + "name": "Register Ownership of Cookie by Elmo", + "businessEventName": "EVcrOwnership", + "payload": { + "Name": "CookieOwnedByElmo", + "economicResourceId_EconomicResource_Ownership": "EconomicResource#0", + "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#0" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "Ownership#1", + "attributeName": "name", + "expectedValue": "CookieOwnedByElmo" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + } + ] + }, + { + "name": "Create Economic Event (delivery)", + "businessEventName": "EVcrEconomicEvent", + "payload": { + "Name": "delivery", + "TimeStamp": null, + "CountDependentViews": 0 + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicEvent#0", + "attributeName": "name", + "expectedValue": "delivery" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + }, + { + "boId": "EconomicEvent#0", + "expectedStateName": "business event" + } + ] + }, + { + "name": "Create Economic Event (payment)", + "businessEventName": "EVcrEconomicEvent", + "payload": { + "Name": "payment", + "TimeStamp": null, + "CountDependentViews": 0 + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicEvent#1", + "attributeName": "name", + "expectedValue": "payment" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + }, + { + "boId": "EconomicEvent#0", + "expectedStateName": "business event" + }, + { + "boId": "EconomicEvent#1", + "expectedStateName": "business event" + } + ] + } + ] +} +```` + +When preparing the test suite specification, do not forger to set the public keys of the participants that are created, and to update them everytime a new network is created. +Indeed, without at least one valid particpant, once the collaboration is initialized, all business events invocation will fail. +The public keys can be retrieved by invoking the _getSenderPk_ transaction on the chaincode, using the different wallets that are available, in the IBM Platform Blockchain VSCode extension. + +When expected attribute values or expected states tests fail, an indication will be displayed in the test results. +Note that this is a _preliminary_ version of the software and that not all errors are handled properly. When errors occur, it might therefore be useful to read the HLF network's logs to have further details on the errors that occurred. + +# Setting up the Test Suite +To ensure that a test suite can be executed correctly, you need, for every run of the suite, to: +1. Create a new HLF network +2. Deploy the chaincode on the network +3. Retrieve and export the wallets and connection profiles to use to the appropriate files +4. Retrieve the public key(s) of the participants to create + +The program will then be executed as follows: +1. Connect to the HLF network and retrieve the chaincode +2. Initialize the B-MERODE collaboration using the sender of the transaction as participants handler +3. Execute each test in the test suite, even if some of them fail, and output the results +4. Output the test suite results summary +5. Close the connection to the HLF network diff --git a/README.md b/README.md index 608ebe7..87f7a07 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,94 @@ -# ChaincodeTestr +# ChaincodeTestr v1.0 [![Build](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml/badge.svg)](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml) +CLI application that supports the execution of test suites on B-MERODE chaincodes deployed on Hyperledger Fabric networks, with integrated multi-organization support for easycc workspaces. -CLI application that supports the execution of a test suite on a B-MERODE chaincode, deployed on a HLF network. -The below video provides the explanation and a demonstration. The remainder of the README provides all the detailed explanations. The test suite used as demonstration has been developed for a B-MERODE model developed by Wim Laurier, and based on the MERODE model presented in the following paper: +## What's New in v1.0 -Laurier, W., Horiuchi, S., & Snoeck, M. (2021). An executable axiomatization of the REA2 ontology. Journal of Information Systems, 35(3), 133-154. +Version 1.0 introduces **multi-organization support** designed to work seamlessly with [easycc](https://github.com/AmaVic/easycc) workspaces: + +- **Multi-Organization Testing**: Execute transactions from different organizations within a single test suite +- **Automatic Credential Discovery**: No need to manually specify wallets and connection profiles +- **Organization Placeholders**: Reference organization public keys using simple placeholders (`$org1`, `$org2`, etc.) +- **Simplified CLI**: Just point to your easycc workspace and test suite +- **Backward Compatible**: Legacy v0.3 mode still available with `--legacy` flag + +For v0.3 documentation, see [README-v0.3.md](README-v0.3.md). + +## Quick Start + +### Installation + +Build the application using Gradle: +```bash +./gradlew build +./gradlew installDist +``` + +The executable will be available at: `build/install/ChaincoderTestr/bin/ChaincoderTestr` -[![B-MERODE Chaincode Tester Demo](https://img.youtube.com/vi/V4gcxtX9kZk/0.jpg)](https://www.youtube.com/watch?v=V4gcxtX9kZk) +### Running a Test Suite (v1.0) +```bash +./ChaincoderTestr --workspace /path/to/easycc/workspace \ + --test-suite-path test-suite.json \ + --chaincode-name myChaincode +``` + +### Legacy Mode (v0.3) + +For backward compatibility with v0.3: +```bash +./ChaincoderTestr --legacy \ + --wallet-path path/to/wallet \ + --identity-name "Org1 Admin" \ + --connection-profile-path path/to/profile.json \ + --chaincode-name myChaincode \ + --test-suite-path test-suite.json +``` -# Running the Application -Using the terminal, go into the bin folder, and you can run the following commands: -```shell -./ChaincoderTestr --help +## Usage + +### Command-Line Options + +#### v1.0 Mode (Default) +``` +Options: + --workspace PATH Path to the easycc workspace root directory + --test-suite-path PATH Path to the test suite JSON file + --chaincode-name TEXT Name of the chaincode to test + -h, --help Show this message and exit ``` -You obtain the following output: -```shell -Usage: test-chaincode [OPTIONS] +#### Legacy Mode (v0.3) +``` Options: - --wallet-path PATH Path to the wallet containing the identity - to use for the connection - --identity-name TEXT Name of the identity to use for the - connection (default: Org1 Admin) - --connection-profile-path PATH Absolute Path to the connection profile + --legacy Enable legacy v0.3 mode + --wallet-path PATH Path to the wallet directory + --identity-name TEXT Identity name (default: "Org1 Admin") + --connection-profile-path PATH Path to connection profile JSON --chaincode-name TEXT Name of the chaincode to test - --test-suite-path PATH Path to the testsuite to run + --test-suite-path PATH Path to the test suite JSON file -h, --help Show this message and exit ``` -To run a test suite, you need to specify all the options above. For instance: -```shell -./ChaincoderTestr --wallet-path chaincode-test/wallets/org1 --identity-name "Org1 Admin" --connection-profile-path chaincode-test/connection-profiles/org1.json --chaincode-name rea --test-suite-path chaincode-test/suites/happy-path-test-suite.json -``` -Assuming that the HLF network is running and that the provided details are correct, the example provides the following output: -````shell -===== Configuring Chaincode Test Suite ===== -Network and Gateway Configuration: {"identityName":"Org1 Admin","channelName":"mychannel","walletPath":"/Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/wallets/org1","connectionProfilePath":"/Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/connection-profiles/org1.json","chaincodeName":"rea"} -Test Suite Specification: /Users/.../Downloads/ChaincoderTestr-1.0-SNAPSHOT/bin/chaincode-test/suites/happy-path-test-suite.json -===== Parsing Test Suite ===== -Test Suite Successfully Parsed: 8 test cases found -===== Connection to the Network ===== -Successfully connected to the network -===== Initializing Collaboration with Sender as Participants Handler ===== -===== Running Test Suite ===== -Test Case: Create First EconomicAgent (Elmo) Successful ✅ -Test Case: Create Second EconomicAgent(Cookie) Successful ✅ -Test Case: Create Economic Resource Resource (Cookie) Successful ✅ -Test Case: Create Economic Resource Resource (Cash) Successful ✅ -Test Case: Register Ownership of Cash by Cookie Successful ✅ -Test Case: Register Ownership of Cookie by Elmo Successful ✅ -Test Case: Create Economic Event (delivery) Failed ❌: Expected transaction to succeed, but it failed (Transaction handleEvent failed: No valid proposal responses received. 2 peer error responses: Error during contract method execution; Error during contract method execution) -Test Case: Create Economic Event (payment) Failed ❌: Expected transaction to succeed, but it failed (Transaction handleEvent failed: No valid proposal responses received. 2 peer error responses: Error during contract method execution; Error during contract method execution) -===== Test Suite Results Summary ===== -Number of Tests: 8 -Number of Successful Tests: 6 -Number of Failed Tests: 2 -===== Closing Gateway Connection ===== -```` -_Note: additional logging output is generated by the HLF gateway client, they can be ignored._ - -# Extracting the Wallets and Connection Profiles -To obtain the wallet folder and the connection profile file, you can use the export functionality of the IBM Blockchain Platform extension for VSCode. - -# Preparing the Test Suite Specification -To specify the test suites, the application relies on a _.json_. - -The fields to specify are the following: -* _name_: the name of the test suite -* _testsCases_: an array of test cases to run - -Each test case is specified as follows: -* _name_: the name of the test case -* _businessEventName_: the name of the business event to fire -* _payload_: the payload of the business event (JSON) -* _expectedToSucceed_: true / false: defines whether the business event is expected to be successfully executed or not -* _thenMarkAsReady_: true / false: defines whether the B-MERODE collaboration shuold be marked as ready after the execution of the business event -* _expectedAttributeValues_: an array of tests to run on the attributes of the business event. Each test is specified as follows: - * _boId_: id of the business object whose attributes values will be tested - * _attributeName_: the name of the attribute (_starting with a lower-case, even if the business object attribute name in the chaincode starts with an upper-case_) - * _expectedValue_: expected value for the attribute -* _expectedBOStates_: an array of tests to run on the states of the business objects. Each test is specified as follows: - * _boId_: id of the business object whose state will be tested - * _expectedStateName_: expected state (name) for the business object - -Note that the _expectedAttributeValues_ and the _expectedBOStates_ are optional. If not specified, the application will not perform any test on the attributes and the states of the business objects. -Also, these fields allow specifying tests for any business object (not only the one created/modified after the execution of the business event), as long as the _boId_ is correctly specified. It is therefore possible to check the propagated effects of business events. - -````json +## Test Suite Specification + +### v1.0 Format + +Test suites are defined in JSON format with organization-aware test cases: + +```json { - "name": "Happy Path REA", + "name": "Multi-Organization Test Suite", "testsCases": [ { - "name": "Create First EconomicAgent (Elmo)", + "name": "Create First Agent as Org1", + "submittedBy": "org1", "businessEventName": "EVcrEconomicAgent", "payload": { - "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOqHVWeIlraubwG+CsmobuigvO0TeaXJbmS8QkkwTOTtBtogKFVjj9PyBt0hekRKy8wtVllQpfu+UgfrITok5BA==", - "Name": "Elmo" + "publicKey": "$org1", + "Name": "Agent 1" }, "expectedToSucceed": true, "thenMarkAsReady": true, @@ -105,7 +96,7 @@ Also, these fields allow specifying tests for any business object (not only the { "boId": "EconomicAgent#0", "attributeName": "name", - "expectedValue": "Elmo" + "expectedValue": "Agent 1" } ], "expectedBOStates": [ @@ -116,301 +107,197 @@ Also, these fields allow specifying tests for any business object (not only the ] }, { - "name": "Create Second EconomicAgent(Cookie)", + "name": "Create Second Agent as Org2", + "submittedBy": "org2", "businessEventName": "EVcrEconomicAgent", "payload": { - "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMkHRF1L6deb2FCwQkZbwNeGtp00kc/sxd2R5XLfiGCM6yA9F4NJXPg1DdNZ45x2YqcDn5lBJKkzHvNMVADMBIw==", - "Name": "Cookie" + "publicKey": "$org2", + "Name": "Agent 2" }, "expectedToSucceed": true, "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "EconomicAgent#1", - "attributeName": "name", - "expectedValue": "Cookie" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - } - ] - }, - { - "name": "Create Economic Resource Resource (Cookie)", - "businessEventName": "EVcrEconomicResource", - "payload": { - "Name": "Cookie" - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "EconomicResource#0", - "attributeName": "name", - "expectedValue": "Cookie" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - } - ] - }, - { - "name": "Create Economic Resource Resource (Cash)", - "businessEventName": "EVcrEconomicResource", - "payload": { - "Name": "Cash" - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "EconomicResource#1", - "attributeName": "name", - "expectedValue": "Cash" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#1", - "expectedStateName": "exists" - } - ] - }, - { - "name": "Register Ownership of Cash by Cookie", - "businessEventName": "EVcrOwnership", - "payload": { - "Name": "CashOwnedByCookie", - "economicResourceId_EconomicResource_Ownership": "EconomicResource#1", - "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#1" - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "Ownership#0", - "attributeName": "name", - "expectedValue": "CashOwnedByCookie" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#1", - "expectedStateName": "exists" - }, - { - "boId": "Ownership#0", - "expectedStateName": "image" - } - ] - }, - { - "name": "Register Ownership of Cookie by Elmo", - "businessEventName": "EVcrOwnership", - "payload": { - "Name": "CookieOwnedByElmo", - "economicResourceId_EconomicResource_Ownership": "EconomicResource#0", - "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#0" - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "Ownership#1", - "attributeName": "name", - "expectedValue": "CookieOwnedByElmo" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#1", - "expectedStateName": "exists" - }, - { - "boId": "Ownership#0", - "expectedStateName": "image" - }, - { - "boId": "Ownership#1", - "expectedStateName": "image" - } - ] - }, - { - "name": "Create Economic Event (delivery)", - "businessEventName": "EVcrEconomicEvent", - "payload": { - "Name": "delivery", - "TimeStamp": null, - "CountDependentViews": 0 - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "EconomicEvent#0", - "attributeName": "name", - "expectedValue": "delivery" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#1", - "expectedStateName": "exists" - }, - { - "boId": "Ownership#0", - "expectedStateName": "image" - }, - { - "boId": "Ownership#1", - "expectedStateName": "image" - }, - { - "boId": "EconomicEvent#0", - "expectedStateName": "business event" - } - ] - }, - { - "name": "Create Economic Event (payment)", - "businessEventName": "EVcrEconomicEvent", - "payload": { - "Name": "payment", - "TimeStamp": null, - "CountDependentViews": 0 - }, - "expectedToSucceed": true, - "thenMarkAsReady": false, - "expectedAttributeValues": [ - { - "boId": "EconomicEvent#1", - "attributeName": "name", - "expectedValue": "payment" - } - ], - "expectedBOStates": [ - { - "boId": "EconomicAgent#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicAgent#1", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#0", - "expectedStateName": "exists" - }, - { - "boId": "EconomicResource#1", - "expectedStateName": "exists" - }, - { - "boId": "Ownership#0", - "expectedStateName": "image" - }, - { - "boId": "Ownership#1", - "expectedStateName": "image" - }, - { - "boId": "EconomicEvent#0", - "expectedStateName": "business event" - }, - { - "boId": "EconomicEvent#1", - "expectedStateName": "business event" - } - ] + "expectedAttributeValues": [ ... ], + "expectedBOStates": [ ... ] } ] } -```` - -When preparing the test suite specification, do not forger to set the public keys of the participants that are created, and to update them everytime a new network is created. -Indeed, without at least one valid particpant, once the collaboration is initialized, all business events invocation will fail. -The public keys can be retrieved by invoking the _getSenderPk_ transaction on the chaincode, using the different wallets that are available, in the IBM Platform Blockchain VSCode extension. - -When expected attribute values or expected states tests fail, an indication will be displayed in the test results. -Note that this is a _preliminary_ version of the software and that not all errors are handled properly. When errors occur, it might therefore be useful to read the HLF network's logs to have further details on the errors that occurred. - -# Setting up the Test Suite -To ensure that a test suite can be executed correctly, you need, for every run of the suite, to: -1. Create a new HLF network -2. Deploy the chaincode on the network -3. Retrieve and export the wallets and connection profiles to use to the appropriate files -4. Retrieve the public key(s) of the participants to create - -The program will then be executed as follows: -1. Connect to the HLF network and retrieve the chaincode -2. Initialize the B-MERODE collaboration using the sender of the transaction as participants handler -3. Execute each test in the test suite, even if some of them fail, and output the results -4. Output the test suite results summary -5. Close the connection to the HLF network +``` + +### Test Case Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Name of the test case | +| `submittedBy` | string | No | Organization name that submits this transaction (defaults to initialization org) | +| `businessEventName` | string | Yes | Name of the business event to invoke | +| `payload` | object | Yes | JSON payload for the transaction (supports `$orgName` placeholders) | +| `expectedToSucceed` | boolean | Yes | Whether the transaction is expected to succeed | +| `thenMarkAsReady` | boolean | Yes | Whether to mark collaboration as ready after this transaction | +| `expectedAttributeValues` | array | No | Expected attribute values to verify after transaction | +| `expectedBOStates` | array | No | Expected business object states to verify after transaction | + +### Key Concepts + +#### 1. Organization Placeholders + +Use `$orgName` syntax in your payload to automatically resolve to the organization's public key: + +```json +{ + "payload": { + "publicKey": "$org1", + "Name": "My Agent" + } +} +``` + +This will be automatically replaced with the actual public key from `workspace/exports/wallets/org1/public_key.txt`. + +#### 2. Initialization Organization + +The initialization organization is automatically determined from the test case with `"thenMarkAsReady": true`. The `submittedBy` field of that test case specifies which organization initializes the collaboration. + +```json +{ + "name": "Initialize Collaboration", + "submittedBy": "org1", + "thenMarkAsReady": true, + ... +} +``` + +#### 3. Transaction Submission + +Each test case can specify a different organization via `submittedBy`. If omitted, it defaults to the initialization organization. + +## EasyCC Workspace Structure + +ChaincodeTestr v1.0 expects the following structure in your easycc workspace: + +``` +workspace/ +└── exports/ + ├── wallets/ + │ ├── org1/ + │ │ ├── admin.id # Admin identity credentials + │ │ ├── public_key.txt # Public key (base64) + │ │ └── metadata.json # Organization metadata + │ └── org2/ + │ └── ... (same structure) + └── connection-profiles/ + ├── connection-org1.json # Connection profile for org1 + └── connection-org2.json # Connection profile for org2 +``` + +The application automatically: +- Discovers all organizations in `exports/wallets/` +- Loads connection profiles from `exports/connection-profiles/connection-{orgName}.json` +- Uses the `admin` identity for all organizations +- Reads public keys from `public_key.txt` files + +## Example Output + +``` +===== ChaincodeTestr v1.0 - Workspace Mode ===== +Workspace: /Users/user/Dev/myworkspace +Test Suite: /Users/user/test-suite.json +Chaincode: myChaincode +===== Initializing Workspace ===== +Discovered 2 organization(s): org1, org2 +===== Parsing Test Suite ===== +Test Suite Successfully Parsed: 8 test cases found +Initialization Organization: org1 (from test case: 'Initialize Collaboration') +===== Initializing Connection Manager ===== +Successfully connected to network as org1 +===== Initializing Collaboration as org1 ===== +Collaboration initialized successfully +===== Running Test Suite ===== + Executing 'Create First Agent as Org1' as org1... +Test Case: Create First Agent as Org1 Successful ✅ + Executing 'Create Second Agent as Org2' as org2... +Test Case: Create Second Agent as Org2 Successful ✅ + ... +===== Test Suite Results Summary ===== +Number of Tests: 8 +Number of Successful Tests: 7 +Number of Failed Tests: 1 +===== Closing Connections ===== +``` + +--- + +## Migration from v0.3 to v1.0 + +### CLI Changes + +**v0.3:** +```bash +./ChaincoderTestr --wallet-path wallets/org1 \ + --identity-name "Org1 Admin" \ + --connection-profile-path profiles/org1.json \ + --chaincode-name myChaincode \ + --test-suite-path test-suite.json +``` + +**v1.0:** +```bash +./ChaincoderTestr --workspace /path/to/easycc/workspace \ + --test-suite-path test-suite.json \ + --chaincode-name myChaincode +``` + +### Test Suite Changes + +**v0.3:** Hardcoded public keys +```json +{ + "payload": { + "publicKey": "MFkwEwYHKoZI...very long key...", + "Name": "Agent" + } +} +``` + +**v1.0:** Organization placeholders + submittedBy +```json +{ + "submittedBy": "org1", + "payload": { + "publicKey": "$org1", + "Name": "Agent" + } +} +``` + +### Running v0.3 Test Suites in v1.0 + +Use the `--legacy` flag to run old test suites without modification: + +```bash +./ChaincoderTestr --legacy \ + --wallet-path wallets/org1 \ + --identity-name "Org1 Admin" \ + --connection-profile-path profiles/org1.json \ + --chaincode-name myChaincode \ + --test-suite-path old-test-suite.json +``` + +## License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. + +--- + +## Related Projects + +- **[easycc](https://github.com/AmaVic/easycc)** - Easy Chaincode CLI for Hyperledger Fabric development +- **[B-MERODE](https://merode.be)** - Model-driven engineering approach for enterprise applications + +## Citation + +If you use this software in your research, please cite: + +Laurier, W., Horiuchi, S., & Snoeck, M. (2021). An executable axiomatization of the REA2 ontology. Journal of Information Systems, 35(3), 133-154. + +## Contributors and Funding +This project is developed and maintained by Victor Amaral de Sousa. It has been requested and funded by Wim Laurier and Satoshi Horiuchi, respectively from the Université Catholique de Louvain (Belgium) and the University of Chuo (Japan). diff --git a/build.gradle.kts b/build.gradle.kts index f775163..39b0f2f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,11 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.7.21" + kotlin("jvm") version "2.0.20" application } group = "be.vamaralds" -version = "0.3" +version = "1.0" repositories { mavenCentral() @@ -28,10 +27,10 @@ tasks.test { useJUnitPlatform() } -tasks.withType { - kotlinOptions.jvmTarget = "1.8" -} - application { mainClass.set("MainKt") -} \ No newline at end of file +} + +kotlin { + jvmToolchain(8) +} diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 8386efb..34ec39d 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,9 +1,24 @@ import be.vamaralds.lib.* import org.hyperledger.fabric.gateway.Contract import java.util.logging.Logger +import java.util.logging.Level +import java.util.logging.LogManager fun main(args: Array) { - System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "off"); + // Keep application logs but suppress HLF verbose logs + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "warn") + System.setProperty("org.slf4j.simpleLogger.log.be.vamaralds", "info") + System.setProperty("org.slf4j.simpleLogger.log.org.hyperledger.fabric.gateway", "error") + System.setProperty("org.slf4j.simpleLogger.log.org.hyperledger.fabric.sdk", "error") + System.setProperty("org.slf4j.simpleLogger.log.io.grpc", "error") + System.setProperty("org.slf4j.simpleLogger.showDateTime", "false") + System.setProperty("org.slf4j.simpleLogger.showThreadName", "false") + System.setProperty("org.slf4j.simpleLogger.showLogName", "false") + + // Suppress OpenTelemetry span export warnings (java.util.logging) + LogManager.getLogManager().getLogger("").level = Level.SEVERE + Logger.getLogger("io.opentelemetry").level = Level.SEVERE + Logger.getLogger("io.opentelemetry.sdk.internal").level = Level.SEVERE try { TestChaincode().main(args) diff --git a/src/main/kotlin/be/vamaralds/lib/ChaincodeTestCase.kt b/src/main/kotlin/be/vamaralds/lib/ChaincodeTestCase.kt index b8737fb..3db760f 100644 --- a/src/main/kotlin/be/vamaralds/lib/ChaincodeTestCase.kt +++ b/src/main/kotlin/be/vamaralds/lib/ChaincodeTestCase.kt @@ -13,7 +13,8 @@ data class ChaincodeTestCase( val expectedAttributeValues: List> = emptyList(), val expectedBOStates: List = emptyList(), val expectedToSucceed: Boolean = true, - val thenMarkAsReady: Boolean = false + val thenMarkAsReady: Boolean = false, + val submittedBy: String? = null // Organization name that submits this transaction ) { companion object { fun fromJson(json: String): ChaincodeTestCase { @@ -36,7 +37,8 @@ data class ChaincodeTestCase( ExpectedState(boId, expectedStateName) } val expectedToSucceed = jsonObject.getBoolean("expectedToSucceed") - return ChaincodeTestCase(name, businessEventName, payload, expectedAttributeValues, expectedBOStates, expectedToSucceed, thenMarkAsReady) + val submittedBy = if (jsonObject.has("submittedBy")) jsonObject.optString("submittedBy") else null + return ChaincodeTestCase(name, businessEventName, payload, expectedAttributeValues, expectedBOStates, expectedToSucceed, thenMarkAsReady, submittedBy) } } } @@ -46,6 +48,13 @@ sealed interface ChaincodeTestResult { fun toJsonString(): String = JSONObject(this).toString() } -data class SuccessfulChaincodeTestResult(override val testCase: ChaincodeTestCase): ChaincodeTestResult -data class FailedChaincodeTestResult(override val testCase: ChaincodeTestCase, val reasons: List = emptyList()): ChaincodeTestResult +data class SuccessfulChaincodeTestResult( + override val testCase: ChaincodeTestCase, + val validatedObjects: Map = emptyMap() +): ChaincodeTestResult + +data class FailedChaincodeTestResult( + override val testCase: ChaincodeTestCase, + val reasons: List = emptyList() +): ChaincodeTestResult diff --git a/src/main/kotlin/be/vamaralds/lib/ChaincodeTestr.kt b/src/main/kotlin/be/vamaralds/lib/ChaincodeTestr.kt index dd1a293..820befb 100644 --- a/src/main/kotlin/be/vamaralds/lib/ChaincodeTestr.kt +++ b/src/main/kotlin/be/vamaralds/lib/ChaincodeTestr.kt @@ -13,14 +13,14 @@ class IncorrectWalletPathException(msg: String): ChaincodeTestrException(msg) class ChaincodeTestr(val config: ConnectionConfiguration) { @Throws(ChaincodeTestrException::class) fun connect(): Connection { - ChaincodeApplication.logger.info { "Initiating Connection to the HLF network" } + ChaincodeApplication.logger.debug { "Initiating Connection to the HLF network" } //Retrieving Wallet to Interact with the Network val walletPath = Paths.get(config.walletPath) var wallet: Wallet? try { wallet = Wallets.newFileSystemWallet(walletPath) - ChaincodeApplication.logger.info { "Retrieving Connection Wallet: Successful" } + ChaincodeApplication.logger.debug { "Retrieving Connection Wallet: Successful" } } catch (e: Exception) { val error = IncorrectWalletPathException("Failed to read wallet in folder: $walletPath. Make sure that the wallet path is correct") ChaincodeApplication.logger.error(error) { "Retrieving Connection Wallet: Failed: $error" } @@ -48,7 +48,7 @@ class ChaincodeTestr(val config: ConnectionConfiguration) { var gateway: Gateway? try { gateway = gatewayBuilder.connect() - ChaincodeApplication.logger.info { "Connection to the HLF network: Successful" } + ChaincodeApplication.logger.debug { "Connection to the HLF network: Successful" } } catch(e: Exception) { val error = ChaincodeTestrException("Failed to connect to the network: ${e.javaClass.simpleName}: ${e.message}") throw error @@ -59,7 +59,7 @@ class ChaincodeTestr(val config: ConnectionConfiguration) { try { network = gateway.getNetwork(config.channelName) - ChaincodeApplication.logger.info { "Selecting Channel ${config.channelName}: Successful" } + ChaincodeApplication.logger.debug { "Selecting Channel ${config.channelName}: Successful" } } catch(e: Exception) { val error = ChaincodeTestrException("Failed to get the network: ${e.javaClass.simpleName}: ${e.message}") throw error @@ -67,13 +67,13 @@ class ChaincodeTestr(val config: ConnectionConfiguration) { try { contract = network.getContract(config.chaincodeName) - ChaincodeApplication.logger.info { "Retrieving contract ${config.chaincodeName}" } + ChaincodeApplication.logger.debug { "Retrieving contract ${config.chaincodeName}" } } catch(e: Exception) { val error = ChaincodeTestrException("Failed to get the contract: ${e.javaClass.simpleName}: ${e.message}") throw error } - ChaincodeApplication.logger.info { "Connection established to the HLF network and contract ${config.chaincodeName}" } + ChaincodeApplication.logger.debug { "Connection established to the HLF network and contract ${config.chaincodeName}" } return Connection(wallet, gateway, network, contract) } } \ No newline at end of file diff --git a/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt b/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt index b0a0e36..e435073 100644 --- a/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt +++ b/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt @@ -13,15 +13,21 @@ class ContractHandler(private val contract: Contract) { fun runTestSuite(testSuite: ChaincodeTestSuite): ChaincodeTestSuiteResult { val results = testSuite.testsCases.map { testCase -> - Pair(testCase, runTest(testCase)) + Pair(testCase, runSingleTest(testCase)) }.toMap() return ChaincodeTestSuiteResult(testSuite.name, results) } - private fun runTest(test: ChaincodeTestCase): ChaincodeTestResult { - ChaincodeApplication.logger.info { "Running test: ${test.name}" } + fun runSingleTest(test: ChaincodeTestCase): ChaincodeTestResult { + return runSingleTest(test, this) + } + + fun runSingleTest(test: ChaincodeTestCase, validationHandler: ContractHandler): ChaincodeTestResult { + ChaincodeApplication.logger.debug { "Running test: ${test.name}" } val errors = mutableListOf() + val validatedObjects = mutableMapOf() + try { handleEvent(test.businessEventName, test.payload) if(!test.expectedToSucceed) @@ -32,15 +38,16 @@ class ContractHandler(private val contract: Contract) { } catch(e: ContractTransactionException) { if(test.expectedToSucceed) - return FailedChaincodeTestResult(test, listOf("Expected transaction to succeed, but it failed (${e.stackTrace})")) + return FailedChaincodeTestResult(test, listOf("Expected transaction to succeed, but it failed: ${e.message}")) } test.expectedAttributeValues.forEach { expectedAttributeValue -> var boJson: JsonBusinessObject? try { - boJson = getBusinessObject(expectedAttributeValue.boId) + boJson = validationHandler.getBusinessObject(expectedAttributeValue.boId) + validatedObjects[expectedAttributeValue.boId] = boJson } catch(e: ContractTransactionException) { - errors.add("Failed to get Business Object with id: ${expectedAttributeValue.boId}") + errors.add("Failed to get Business Object with id: ${expectedAttributeValue.boId} - ${e.message}") return FailedChaincodeTestResult(test, errors) } @@ -53,9 +60,10 @@ class ContractHandler(private val contract: Contract) { test.expectedBOStates.forEach { expectedState -> var boJson: JsonBusinessObject? try { - boJson = getBusinessObject(expectedState.boId) + boJson = validationHandler.getBusinessObject(expectedState.boId) + validatedObjects[expectedState.boId] = boJson } catch(e: ContractTransactionException) { - errors.add("Failed to get Business Object with id: ${expectedState.boId}") + errors.add("Failed to get Business Object with id: ${expectedState.boId} - ${e.message}") return FailedChaincodeTestResult(test, errors) } @@ -66,7 +74,7 @@ class ContractHandler(private val contract: Contract) { } return if(errors.isEmpty()) - SuccessfulChaincodeTestResult(test) + SuccessfulChaincodeTestResult(test, validatedObjects) else FailedChaincodeTestResult(test, errors) } @@ -118,10 +126,10 @@ class ContractHandler(private val contract: Contract) { @Throws(ContractTransactionException::class) fun submitTransaction(transactionName: String, vararg args: String): String { - ChaincodeApplication.logger.info { "Submitting transaction $transactionName with args: ${args.joinToString()}" } + ChaincodeApplication.logger.debug { "Submitting transaction $transactionName with args: ${args.joinToString()}" } try { val result = contract.submitTransaction(transactionName, *args).toString(Charsets.UTF_8) - ChaincodeApplication.logger.info { "Transaction $transactionName submitted successfully - Result: $result" } + ChaincodeApplication.logger.debug { "Transaction $transactionName submitted successfully - Result: $result" } return result } catch(e: Exception) { when(e) { @@ -151,10 +159,10 @@ class ContractHandler(private val contract: Contract) { @Throws(ContractTransactionException::class) fun evaluateTransaction(transactionName: String, vararg args: String): String { - ChaincodeApplication.logger.info { "Evaluating transaction $transactionName with args: ${args.joinToString()}" } + ChaincodeApplication.logger.debug { "Evaluating transaction $transactionName with args: ${args.joinToString()}" } try { val result = contract.evaluateTransaction(transactionName, *args).toString(Charsets.UTF_8) - ChaincodeApplication.logger.info { "Transaction $transactionName evaluated successfully - Result: $result" } + ChaincodeApplication.logger.debug { "Transaction $transactionName evaluated successfully - Result: $result" } return result } catch(e: Exception) { when(e) { diff --git a/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt b/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt new file mode 100644 index 0000000..bb180c4 --- /dev/null +++ b/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt @@ -0,0 +1,76 @@ +package be.vamaralds.lib + +/** + * Manages multiple connections to the network, one for each organization + * This allows switching between organizations when submitting transactions + */ +class MultiOrgConnectionManager( + private val workspaceManager: WorkspaceManager, + private val chaincodeName: String +) { + private val connections = mutableMapOf() + private var channelName: String? = null + + /** + * Gets or creates a connection for a specific organization + */ + fun getConnection(orgName: String): Connection { + // Validate organization exists + workspaceManager.validateOrganization(orgName) + + // Return existing connection if available + if (connections.containsKey(orgName)) { + return connections[orgName]!! + } + + // Create new connection + ChaincodeApplication.logger.debug { "Creating connection for organization: $orgName" } + + val config = ConnectionConfiguration( + walletPath = workspaceManager.getWalletPath(orgName), + identityName = workspaceManager.getIdentityName(orgName), + connectionProfilePath = workspaceManager.getConnectionProfilePath(orgName), + channelName = getChannelName(orgName), + chaincodeName = chaincodeName + ) + + val connection = ChaincodeTestr(config).connect() + connections[orgName] = connection + + ChaincodeApplication.logger.debug { "Successfully created connection for organization: $orgName" } + return connection + } + + /** + * Gets the channel name (discovers on first call, then caches) + */ + private fun getChannelName(orgName: String): String { + if (channelName == null) { + channelName = workspaceManager.getChannelName(orgName) + ChaincodeApplication.logger.debug { "Discovered channel name: $channelName" } + } + return channelName!! + } + + /** + * Gets all available organizations + */ + fun getAvailableOrganizations(): List { + return workspaceManager.getOrganizations() + } + + /** + * Closes all open connections + */ + fun closeAll() { + ChaincodeApplication.logger.info { "Closing all connections (${connections.size} organizations)" } + connections.values.forEach { connection -> + try { + connection.gateway.close() + } catch (e: Exception) { + ChaincodeApplication.logger.warn { "Error closing connection: ${e.message}" } + } + } + connections.clear() + } +} diff --git a/src/main/kotlin/be/vamaralds/lib/PayloadResolver.kt b/src/main/kotlin/be/vamaralds/lib/PayloadResolver.kt new file mode 100644 index 0000000..13aafa9 --- /dev/null +++ b/src/main/kotlin/be/vamaralds/lib/PayloadResolver.kt @@ -0,0 +1,75 @@ +package be.vamaralds.lib + +import org.json.JSONArray +import org.json.JSONObject + +/** + * Resolves organization placeholders in test case payloads + * Replaces $orgName with actual public keys from the workspace + */ +class PayloadResolver(private val workspaceManager: WorkspaceManager) { + + /** + * Resolves all organization placeholders in a JSON payload string + * Example: {"publicKey": "$org1"} becomes {"publicKey": "MFkw..."} + */ + fun resolvePayload(payloadJson: String): String { + val jsonObject = JSONObject(payloadJson) + resolveJsonObject(jsonObject) + return jsonObject.toString() + } + + /** + * Recursively resolves placeholders in a JSON object + */ + private fun resolveJsonObject(jsonObject: JSONObject) { + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = jsonObject.get(key) + + when (value) { + is String -> { + if (value.startsWith("$")) { + val orgName = value.substring(1) + try { + val publicKey = workspaceManager.getPublicKey(orgName) + jsonObject.put(key, publicKey) + ChaincodeApplication.logger.debug { "Resolved placeholder '$value' to public key for organization: $orgName" } + } catch (e: WorkspaceException) { + ChaincodeApplication.logger.warn { "Could not resolve placeholder '$value': ${e.message}" } + } + } + } + is JSONObject -> resolveJsonObject(value) + is JSONArray -> resolveJsonArray(value) + } + } + } + + /** + * Recursively resolves placeholders in a JSON array + */ + private fun resolveJsonArray(jsonArray: JSONArray) { + for (i in 0 until jsonArray.length()) { + val value = jsonArray.get(i) + + when (value) { + is String -> { + if (value.startsWith("$")) { + val orgName = value.substring(1) + try { + val publicKey = workspaceManager.getPublicKey(orgName) + jsonArray.put(i, publicKey) + ChaincodeApplication.logger.debug { "Resolved placeholder '$value' to public key for organization: $orgName" } + } catch (e: WorkspaceException) { + ChaincodeApplication.logger.warn { "Could not resolve placeholder '$value': ${e.message}" } + } + } + } + is JSONObject -> resolveJsonObject(value) + is JSONArray -> resolveJsonArray(value) + } + } + } +} diff --git a/src/main/kotlin/be/vamaralds/lib/TestChaincode.kt b/src/main/kotlin/be/vamaralds/lib/TestChaincode.kt index b6f16b0..eea4472 100644 --- a/src/main/kotlin/be/vamaralds/lib/TestChaincode.kt +++ b/src/main/kotlin/be/vamaralds/lib/TestChaincode.kt @@ -2,19 +2,39 @@ package be.vamaralds.lib import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.mordant.table.table import com.github.ajalt.mordant.terminal.Terminal class TestChaincode: CliktCommand() { - val walletPath by option(help = "Path to the wallet containing the identity to use for the connection").path() - val identityName by option(help = "Name of the identity to use for the connection (default: Org1 Admin)").default("Org1 Admin") - val connectionProfilePath by option(help = "Absolute Path to the connection profile").path() + // Legacy CLI parameters (v0.3) + val legacy by option(help = "Use legacy v0.3 CLI interface").flag(default = false) + val walletPath by option(help = "[LEGACY] Path to the wallet containing the identity to use for the connection").path() + val identityName by option(help = "[LEGACY] Name of the identity to use for the connection").default("Org1 Admin") + val connectionProfilePath by option(help = "[LEGACY] Absolute Path to the connection profile").path() + + // New CLI parameters (v1.0) + val workspace by option(help = "Path to the easycc workspace root").path() + + // Common parameters val chaincodeName by option(help = "Name of the chaincode to test") val testSuitePath by option(help = "Path to the testsuite to run").path() override fun run() { + if (legacy) { + runLegacyMode() + } else { + runWorkspaceMode() + } + } + + private fun runLegacyMode() { + @OptIn(com.github.ajalt.mordant.terminal.ExperimentalTerminalApi::class) + val terminal = Terminal() + terminal.println("===== Running in Legacy Mode (v0.3) =====") + val config = ConnectionConfiguration( walletPath = walletPath!!.toAbsolutePath().toString(), identityName = identityName, @@ -22,7 +42,6 @@ class TestChaincode: CliktCommand() { chaincodeName = chaincodeName!! ) - val terminal = Terminal() terminal.println("===== Configuring Chaincode Test Suite =====") terminal.println("Network and Gateway Configuration: $config") terminal.println("Test Suite Specification: ${testSuitePath!!.toAbsolutePath()}") @@ -68,14 +87,192 @@ class TestChaincode: CliktCommand() { val nbSuccess = testSuiteResults.results.count { it.value is SuccessfulChaincodeTestResult } val nbFailed = testSuiteResults.results.count { it.value is FailedChaincodeTestResult } + val nbSkipped = testSuite.testsCases.size - testSuiteResults.results.size + terminal.println("===== Test Suite Results Summary =====") terminal.println("Number of Tests: ${testSuite.testsCases.size}") terminal.println("Number of Successful Tests: $nbSuccess") terminal.println("Number of Failed Tests: $nbFailed") + if (nbSkipped > 0) { + terminal.println("Number of Skipped Tests: $nbSkipped (stopped after first failure)") + } terminal.println("===== Closing Gateway Connection =====") connection.gateway.close() } + private fun runWorkspaceMode() { + @OptIn(com.github.ajalt.mordant.terminal.ExperimentalTerminalApi::class) + val terminal = Terminal() + terminal.println("===== ChaincodeTestr v1.0 - Workspace Mode =====") + + // Validate required parameters + if (workspace == null || testSuitePath == null || chaincodeName == null) { + terminal.println("Error: Missing required parameters") + terminal.println("Required: --workspace, --test-suite-path, --chaincode-name") + return + } + + terminal.println("Workspace: ${workspace!!.toAbsolutePath()}") + terminal.println("Test Suite: ${testSuitePath!!.toAbsolutePath()}") + terminal.println("Chaincode: $chaincodeName") + + // Initialize workspace manager + terminal.println("===== Initializing Workspace =====") + var workspaceManager: WorkspaceManager? + try { + workspaceManager = WorkspaceManager(workspace!!.toAbsolutePath().toString()) + val orgs = workspaceManager.getOrganizations() + terminal.println("Discovered ${orgs.size} organization(s): ${orgs.joinToString(", ")}") + } catch(e: WorkspaceException) { + terminal.println("Failed to initialize workspace: ${e.message}") + return + } + + // Parse test suite + terminal.println("===== Parsing Test Suite =====") + var testSuite: ChaincodeTestSuite? + try { + testSuite = ChaincodeTestSuite.fromJson(testSuitePath!!.toFile().readText()) + terminal.println("Test Suite Successfully Parsed: ${testSuite.testsCases.size} test cases found") + } catch(e: Exception) { + terminal.println("Failed to parse the test suite: ${e.message}") + return + } + + // Find initialization organization (test case with thenMarkAsReady: true) + val initTestCase = testSuite.testsCases.find { it.thenMarkAsReady } + if (initTestCase == null) { + terminal.println("Error: No test case found with 'thenMarkAsReady: true'. Cannot determine initialization organization.") + return + } + + val initOrg = initTestCase.submittedBy + if (initOrg == null) { + terminal.println("Error: Initialization test case '${initTestCase.name}' must have 'submittedBy' field specified.") + return + } + + terminal.println("Initialization Organization: $initOrg (from test case: '${initTestCase.name}')") + + // Initialize multi-org connection manager + terminal.println("===== Initializing Connection Manager =====") + val connectionManager = MultiOrgConnectionManager(workspaceManager, chaincodeName!!) + val payloadResolver = PayloadResolver(workspaceManager) + + try { + // Get initial connection + val initConnection = connectionManager.getConnection(initOrg) + terminal.println("Successfully connected to network as $initOrg") + + // Initialize collaboration + terminal.println("===== Initializing Collaboration as $initOrg =====") + val initContractHandler = ContractHandler(initConnection.contract) + try { + initContractHandler.initializeCollaboration() + terminal.println("Collaboration initialized successfully") + } catch(e: ContractTransactionException) { + terminal.println("Failed to initialize the collaboration: ${e.message}") + connectionManager.closeAll() + return + } + + // Run test suite + terminal.println("\n========================================") + terminal.println(" Running Test Suite") + terminal.println("========================================") + val testSuiteResults = runMultiOrgTestSuite( + testSuite, + connectionManager, + payloadResolver, + initOrg, + initContractHandler, + terminal + ) + + // Display summary + val nbSuccess = testSuiteResults.results.count { it.value is SuccessfulChaincodeTestResult } + val nbFailed = testSuiteResults.results.count { it.value is FailedChaincodeTestResult } + val nbSkipped = testSuite.testsCases.size - testSuiteResults.results.size + + terminal.println("\n========================================") + terminal.println(" SUMMARY") + terminal.println("========================================") + terminal.println("Total Tests: ${testSuite.testsCases.size}") + terminal.println("Passed: $nbSuccess ✅") + terminal.println("Failed: $nbFailed ❌") + if (nbSkipped > 0) { + terminal.println("Skipped: $nbSkipped ⏭️ (stopped after failure)") + } + terminal.println("========================================") + + terminal.println("===== Closing Connections =====") + connectionManager.closeAll() + + } catch(e: Exception) { + terminal.println("Error during test execution: ${e.message}") + connectionManager.closeAll() + } + } + + private fun runMultiOrgTestSuite( + testSuite: ChaincodeTestSuite, + connectionManager: MultiOrgConnectionManager, + payloadResolver: PayloadResolver, + defaultOrg: String, + validationHandler: ContractHandler, + terminal: Terminal + ): ChaincodeTestSuiteResult { + val results = mutableMapOf() + + for ((index, testCase) in testSuite.testsCases.withIndex()) { + val orgName = testCase.submittedBy ?: defaultOrg + val testNumber = index + 1 + val totalTests = testSuite.testsCases.size + + terminal.println("\n[Test $testNumber/$totalTests]") + terminal.println(" Name: ${testCase.name}") + terminal.println(" Org: $orgName") + terminal.print(" Status: Running...") + + val connection = connectionManager.getConnection(orgName) + val contractHandler = ContractHandler(connection.contract) + + // Resolve payload placeholders + val resolvedPayload = payloadResolver.resolvePayload(testCase.payload) + val resolvedTestCase = testCase.copy(payload = resolvedPayload) + + // Submit transaction from the specified org, but validate from init org's perspective + val result = contractHandler.runSingleTest(resolvedTestCase, validationHandler) + results[testCase] = result + + // Show result immediately + when(val resVal = result) { + is SuccessfulChaincodeTestResult -> { + terminal.println("\r Status: ✅ PASSED") + + // Display created/modified objects + if (resVal.validatedObjects.isNotEmpty()) { + val primaryObject = resVal.validatedObjects.entries.firstOrNull() + primaryObject?.let { + terminal.println(" Result: Created/Modified ${it.key}") + val name = try { it.value.getAttributeValue("name") } catch(e: Exception) { null } + if (name != null) { + terminal.println(" └─ name: $name") + } + } + } + } + is FailedChaincodeTestResult -> { + terminal.println("\r Status: ❌ FAILED") + terminal.println(" Error: ${resVal.reasons.joinToString(", ")}") + terminal.println("\n⚠️ Test failed - stopping test suite execution") + break // Stop on first failure + } + } + } + + return ChaincodeTestSuiteResult(testSuite.name, results) + } +} -} \ No newline at end of file diff --git a/src/main/kotlin/be/vamaralds/lib/WorkspaceManager.kt b/src/main/kotlin/be/vamaralds/lib/WorkspaceManager.kt new file mode 100644 index 0000000..ad473d0 --- /dev/null +++ b/src/main/kotlin/be/vamaralds/lib/WorkspaceManager.kt @@ -0,0 +1,120 @@ +package be.vamaralds.lib + +import org.json.JSONObject +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +class WorkspaceException(message: String) : Exception(message) + +/** + * Manages discovery and access to organization credentials in an easycc workspace + */ +class WorkspaceManager(private val workspacePath: String) { + + private val exportsPath = Paths.get(workspacePath, "exports") + private val walletsPath = exportsPath.resolve("wallets") + private val connectionProfilesPath = exportsPath.resolve("connection-profiles") + + init { + if (!Files.exists(exportsPath)) { + throw WorkspaceException("Workspace exports directory not found at: $exportsPath") + } + if (!Files.exists(walletsPath)) { + throw WorkspaceException("Wallets directory not found at: $walletsPath") + } + if (!Files.exists(connectionProfilesPath)) { + throw WorkspaceException("Connection profiles directory not found at: $connectionProfilesPath") + } + } + + /** + * Discovers all available organizations in the workspace + */ + fun getOrganizations(): List { + val walletsDir = walletsPath.toFile() + return walletsDir.listFiles() + ?.filter { it.isDirectory && !it.name.startsWith(".") } + ?.map { it.name } + ?.sorted() + ?: emptyList() + } + + /** + * Gets the wallet path for a specific organization + */ + fun getWalletPath(orgName: String): String { + val walletPath = walletsPath.resolve(orgName) + if (!Files.exists(walletPath)) { + throw WorkspaceException("Wallet not found for organization: $orgName at $walletPath") + } + return walletPath.toAbsolutePath().toString() + } + + /** + * Gets the connection profile path for a specific organization + */ + fun getConnectionProfilePath(orgName: String): String { + val profilePath = connectionProfilesPath.resolve("connection-${orgName}.json") + if (!Files.exists(profilePath)) { + throw WorkspaceException("Connection profile not found for organization: $orgName at $profilePath") + } + return profilePath.toAbsolutePath().toString() + } + + /** + * Gets the identity name for a specific organization (always "admin") + */ + fun getIdentityName(orgName: String): String { + return "admin" + } + + /** + * Gets the public key for a specific organization from public_key.txt + */ + fun getPublicKey(orgName: String): String { + val publicKeyPath = walletsPath.resolve(orgName).resolve("public_key.txt") + if (!Files.exists(publicKeyPath)) { + throw WorkspaceException("Public key file not found for organization: $orgName at $publicKeyPath") + } + return publicKeyPath.toFile().readText().trim() + } + + /** + * Gets the channel name from a connection profile + * Returns the first channel found, or "mychannel" as default + */ + fun getChannelName(orgName: String): String { + try { + val profilePath = getConnectionProfilePath(orgName) + val profileContent = File(profilePath).readText() + val jsonObject = JSONObject(profileContent) + + if (jsonObject.has("channels")) { + val channels = jsonObject.getJSONObject("channels") + val channelNames = channels.keys() + if (channelNames.hasNext()) { + return channelNames.next() + } + } + } catch (e: Exception) { + ChaincodeApplication.logger.warn { "Could not discover channel name from connection profile: ${e.message}" } + } + + // Default fallback + return "mychannel" + } + + /** + * Validates that an organization exists in the workspace + */ + fun validateOrganization(orgName: String) { + val orgs = getOrganizations() + if (!orgs.contains(orgName)) { + throw WorkspaceException( + "Organization '$orgName' not found in workspace. Available organizations: ${orgs.joinToString(", ")}" + ) + } + } +} diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties new file mode 100644 index 0000000..11ece2e --- /dev/null +++ b/src/main/resources/logging.properties @@ -0,0 +1,11 @@ +# Java Util Logging Configuration +# Suppress OpenTelemetry span export warnings +io.opentelemetry.level=SEVERE +io.opentelemetry.sdk.level=SEVERE +io.opentelemetry.sdk.internal.level=SEVERE + +# Default console handler level +.level=WARNING +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=WARNING +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..c6a03d1 --- /dev/null +++ b/src/main/resources/simplelogger.properties @@ -0,0 +1,19 @@ +# SLF4J Simple Logger Configuration +# Set default log level to INFO +org.slf4j.simpleLogger.defaultLogLevel=info + +# Suppress Hyperledger Fabric Gateway verbose logging +org.slf4j.simpleLogger.log.org.hyperledger.fabric.gateway=error +org.slf4j.simpleLogger.log.org.hyperledger.fabric.sdk=error + +# Show only errors from gRPC +org.slf4j.simpleLogger.log.io.grpc=error + +# Keep application logging at INFO level +org.slf4j.simpleLogger.log.be.vamaralds=info + +# Show date/time in logs +org.slf4j.simpleLogger.showDateTime=false +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=false +org.slf4j.simpleLogger.showShortLogName=true diff --git a/src/test/resources/multi-org-test-suite.json b/src/test/resources/multi-org-test-suite.json new file mode 100644 index 0000000..bb92356 --- /dev/null +++ b/src/test/resources/multi-org-test-suite.json @@ -0,0 +1,311 @@ +{ + "name": "Multi-Organization REA Demo", + "testsCases": [ + { + "name": "Create First EconomicAgent (Elmo) as Org1", + "submittedBy": "org1", + "businessEventName": "EVcrEconomicAgent", + "payload": { + "publicKey": "$org1", + "Name": "Elmo" + }, + "expectedToSucceed": true, + "thenMarkAsReady": true, + "expectedAttributeValues": [ + { + "boId": "EconomicAgent#0", + "attributeName": "name", + "expectedValue": "Elmo" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Second EconomicAgent (Cookie) as Org1 for Org2", + "submittedBy": "org1", + "businessEventName": "EVcrEconomicAgent", + "payload": { + "publicKey": "$org2", + "Name": "Cookie" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicAgent#1", + "attributeName": "name", + "expectedValue": "Cookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Economic Resource (Cookie) from Org1", + "submittedBy": "org1", + "businessEventName": "EVcrEconomicResource", + "payload": { + "Name": "Cookie" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicResource#0", + "attributeName": "name", + "expectedValue": "Cookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Create Economic Resource (Cash) from Org2", + "submittedBy": "org2", + "businessEventName": "EVcrEconomicResource", + "payload": { + "Name": "Cash" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicResource#1", + "attributeName": "name", + "expectedValue": "Cash" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + } + ] + }, + { + "name": "Register Ownership of Cash by Cookie (from Org2)", + "submittedBy": "org2", + "businessEventName": "EVcrOwnership", + "payload": { + "Name": "CashOwnedByCookie", + "economicResourceId_EconomicResource_Ownership": "EconomicResource#1", + "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#1" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "Ownership#0", + "attributeName": "name", + "expectedValue": "CashOwnedByCookie" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + } + ] + }, + { + "name": "Register Ownership of Cookie by Elmo (from Org1)", + "submittedBy": "org1", + "businessEventName": "EVcrOwnership", + "payload": { + "Name": "CookieOwnedByElmo", + "economicResourceId_EconomicResource_Ownership": "EconomicResource#0", + "economicAgentId_EconomicAgent_Ownership": "EconomicAgent#0" + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "Ownership#1", + "attributeName": "name", + "expectedValue": "CookieOwnedByElmo" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + } + ] + }, + { + "name": "Create Economic Event (delivery) from Org1", + "submittedBy": "org1", + "businessEventName": "EVcrEconomicEvent", + "payload": { + "Name": "delivery", + "TimeStamp": null, + "CountDependentViews": 0 + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicEvent#0", + "attributeName": "name", + "expectedValue": "delivery" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + }, + { + "boId": "EconomicEvent#0", + "expectedStateName": "businessEvent" + } + ] + }, + { + "name": "Create Economic Event (payment) from Org2", + "submittedBy": "org2", + "businessEventName": "EVcrEconomicEvent", + "payload": { + "Name": "payment", + "TimeStamp": null, + "CountDependentViews": 0 + }, + "expectedToSucceed": true, + "thenMarkAsReady": false, + "expectedAttributeValues": [ + { + "boId": "EconomicEvent#1", + "attributeName": "name", + "expectedValue": "payment" + } + ], + "expectedBOStates": [ + { + "boId": "EconomicAgent#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicAgent#1", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#0", + "expectedStateName": "exists" + }, + { + "boId": "EconomicResource#1", + "expectedStateName": "exists" + }, + { + "boId": "Ownership#0", + "expectedStateName": "image" + }, + { + "boId": "Ownership#1", + "expectedStateName": "image" + }, + { + "boId": "EconomicEvent#0", + "expectedStateName": "business event" + }, + { + "boId": "EconomicEvent#1", + "expectedStateName": "business event" + } + ] + } + ] +} diff --git a/task.md b/task.md new file mode 100644 index 0000000..fd8cb5e --- /dev/null +++ b/task.md @@ -0,0 +1,30 @@ +The software we have here is designed to run test suites on chaincodes running on Hyperledger network. + +Check out the README to see how to use it, and how "test suites" are specified. You can also find examples of test suites there: ./src/test/resources/happy-path-test-suite.json + +When you want to run a test suite on a deploy chaincode on a running network, we use the following command: +./ChaincoderTestr --wallet-path chaincode-test/wallets/org1 --identity-name "Org1 Admin" --connection-profile-path chaincode-test/connection-profiles/org1.json --chaincode-name chaincode --test-suite-path +... + +Currently, the software has atwo limitation I want to lift. It is that you have to specify the wallet/connection-profile pair you want to use to execute transactions; and that in the test suite files, when a public key has to be specified, you have to provide it manually. + +In the new setup that I want, a user should be able to specicfy, directly in the test suite, as which organization it wants to submit a transaction, by specifying the "name" or the organization. The software should then fetch the details of the corresponding wallet/connection profile to submit the transaction. Also, when referring to public keys in the testsuite, again, specifying the name of the organization instead would be much easier and repetable. + +The new version will work alongside another software, called easycc. It supports the creation and deployment of HLF networks and chaincodes, and exports credentials for multiple organizations. You can find an example of folder that contains a workspace of easycc, with details on the credentials/wallet, etc. in the directories, and which includes multiple organizations. If I create a network with two organizations (say "supplier" and "provider"), I should be able to say, for each transaction in the test suite, whether I want to run it as supplier or provider, and be able to specify the "supplier's public key" by mentioning "supplier" instead of the actual public key (same for all types of participants). The type of participant / name of organization is not pre-defined but is user-defined when the network is created and the test suite is written. + +Here's an example of how to run the program current version: ./ChaincoderTestr --wallet-path /Users/vamarald/Dev/testn/exports/wallets/org1 --identity-name "Org1" --connection-profile-path /Users/vamarald/Dev/testn/exports/connection-profiles/connection-org1.json --chaincode-name chaincode --test-suite-path /Users/vamarald/Dev/ChaincodeTestr/src/test/resources/happy-path-test-suite.json + +Here's how I want to be able to run the new version, with the other changes above: +./Chaincodetestr --worskspace ROOT_OF_EASYCC_WORKSPACE +--test-suite-path PATH_TO_TEST_SUITE +--init-as NAME_OF_INITIALIZING_ORGANIZATION +--chaincode-name CC_NAME +--chaincode-version VERSION + +You can look in-depth in: +~/Dev/testn: to see the content of a easycc sample workspace +~/Dev/easycc: to see the full source code of easycc, to know how it maps organization names to outputs in different ways (wallets, connection profiles, etc.). + +Also, keep a markdown file with the original README (for version 0.3), but create a new, update README.md that will include updated documentation, details on the changes from v0.3 to v1.0 (the version we are going to implement), and a link to the older version's readme. + +Before you make any changes, make a thorough review of the concerned softwares, and propose me a comprehensive plan for the implementation of the changes that are required. If you have any question or clarification required, ask it now if possible, but later questions are of course allowed. From 67a39dc3336a047cb26d42dae09766a93b4aade7 Mon Sep 17 00:00:00 2001 From: "Victor Amaral de Sousa, PhD" Date: Sat, 14 Feb 2026 08:34:56 +0100 Subject: [PATCH 2/4] update readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87f7a07..546bfa8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ChaincodeTestr v1.0 [![Build](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml/badge.svg)](https://github.com/AmaVic/ChaincodeTestr/actions/workflows/build.yml) -CLI application that supports the execution of test suites on B-MERODE chaincodes deployed on Hyperledger Fabric networks, with integrated multi-organization support for easycc workspaces. +CLI application that supports the execution of test suites on B-MERODE chaincodes deployed on Hyperledger Fabric networks, with integrated multi-organization support for [**easycc**](https://github.com/AmaVic/easycc) workspaces. ## What's New in v1.0 -Version 1.0 introduces **multi-organization support** designed to work seamlessly with [easycc](https://github.com/AmaVic/easycc) workspaces: +Version 1.0 introduces **multi-organization support** designed to work seamlessly with [**easycc**](https://github.com/AmaVic/easycc) workspaces: - **Multi-Organization Testing**: Execute transactions from different organizations within a single test suite - **Automatic Credential Discovery**: No need to manually specify wallets and connection profiles @@ -172,7 +172,9 @@ Each test case can specify a different organization via `submittedBy`. If omitte ## EasyCC Workspace Structure -ChaincodeTestr v1.0 expects the following structure in your easycc workspace: +ChaincodeTestr v1.0 is designed to work with workspaces generated by [**easycc**](https://github.com/AmaVic/easycc), a tool for easily deploying and managing Hyperledger Fabric networks with multiple organizations. + +The tool expects the following structure in your easycc workspace: ``` workspace/ From fca9f653af304f3dba759db353168881d3233c1e Mon Sep 17 00:00:00 2001 From: "Victor Amaral de Sousa, PhD" Date: Sat, 14 Feb 2026 09:20:47 +0100 Subject: [PATCH 3/4] update readme --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++---------- task.md | 30 ------------------- 2 files changed, 74 insertions(+), 46 deletions(-) delete mode 100644 task.md diff --git a/README.md b/README.md index 546bfa8..a990c7f 100644 --- a/README.md +++ b/README.md @@ -201,29 +201,89 @@ The application automatically: ``` ===== ChaincodeTestr v1.0 - Workspace Mode ===== -Workspace: /Users/user/Dev/myworkspace -Test Suite: /Users/user/test-suite.json -Chaincode: myChaincode +Workspace: /Users/vamarald/Dev/testn +Test Suite: /Users/vamarald/Dev/ChaincodeTestr/src/test/resources/multi-org-test-suite.json +Chaincode: mychaincode ===== Initializing Workspace ===== Discovered 2 organization(s): org1, org2 ===== Parsing Test Suite ===== Test Suite Successfully Parsed: 8 test cases found -Initialization Organization: org1 (from test case: 'Initialize Collaboration') +Initialization Organization: org1 (from test case: 'Create First EconomicAgent (Elmo) as Org1') ===== Initializing Connection Manager ===== Successfully connected to network as org1 ===== Initializing Collaboration as org1 ===== Collaboration initialized successfully -===== Running Test Suite ===== - Executing 'Create First Agent as Org1' as org1... -Test Case: Create First Agent as Org1 Successful ✅ - Executing 'Create Second Agent as Org2' as org2... -Test Case: Create Second Agent as Org2 Successful ✅ - ... -===== Test Suite Results Summary ===== -Number of Tests: 8 -Number of Successful Tests: 7 -Number of Failed Tests: 1 + +======================================== + Running Test Suite +======================================== + +[Test 1/8] + Name: Create First EconomicAgent (Elmo) as Org1 + Org: org1 + Status: ✅ PASSED. + Result: Created/Modified EconomicAgent#0 + └─ name: Elmo + +[Test 2/8] + Name: Create Second EconomicAgent (Cookie) as Org1 for Org2 + Org: org1 + Status: ✅ PASSED. + Result: Created/Modified EconomicAgent#1 + └─ name: Cookie + +[Test 3/8] + Name: Create Economic Resource (Cookie) from Org1 + Org: org1 + Status: ✅ PASSED. + Result: Created/Modified EconomicResource#0 + └─ name: Cookie + +[Test 4/8] + Name: Create Economic Resource (Cash) from Org2 + Org: org2 + Status: ✅ PASSED. + Result: Created/Modified EconomicResource#1 + └─ name: Cash + +[Test 5/8] + Name: Register Ownership of Cash by Cookie (from Org2) + Org: org2 + Status: ✅ PASSED. + Result: Created/Modified Ownership#0 + └─ name: CashOwnedByCookie + +[Test 6/8] + Name: Register Ownership of Cookie by Elmo (from Org1) + Org: org1 + Status: ✅ PASSED. + Result: Created/Modified Ownership#1 + └─ name: CookieOwnedByElmo + +[Test 7/8] + Name: Create Economic Event (delivery) from Org1 + Org: org1 + Status: ✅ PASSED. + Result: Created/Modified EconomicEvent#0 + └─ name: delivery + +[Test 8/8] + Name: Create Economic Event (payment) from Org2 + Org: org2 + Status: ❌ FAILED. + Error: Expected state of Business Object EconomicEvent#0 to be business event, but it was businessEvent, Expected state of Business Object EconomicEvent#1 to be business event, but it was businessEvent + +⚠️ Test failed - stopping test suite execution + +======================================== + SUMMARY +======================================== +Total Tests: 8 +Passed: 7 ✅ +Failed: 1 ❌ +======================================== ===== Closing Connections ===== +INFO ChaincodeApplication - Closing all connections (2 organizations) ``` --- @@ -288,8 +348,6 @@ Use the `--legacy` flag to run old test suites without modification: This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. ---- - ## Related Projects - **[easycc](https://github.com/AmaVic/easycc)** - Easy Chaincode CLI for Hyperledger Fabric development diff --git a/task.md b/task.md deleted file mode 100644 index fd8cb5e..0000000 --- a/task.md +++ /dev/null @@ -1,30 +0,0 @@ -The software we have here is designed to run test suites on chaincodes running on Hyperledger network. - -Check out the README to see how to use it, and how "test suites" are specified. You can also find examples of test suites there: ./src/test/resources/happy-path-test-suite.json - -When you want to run a test suite on a deploy chaincode on a running network, we use the following command: -./ChaincoderTestr --wallet-path chaincode-test/wallets/org1 --identity-name "Org1 Admin" --connection-profile-path chaincode-test/connection-profiles/org1.json --chaincode-name chaincode --test-suite-path -... - -Currently, the software has atwo limitation I want to lift. It is that you have to specify the wallet/connection-profile pair you want to use to execute transactions; and that in the test suite files, when a public key has to be specified, you have to provide it manually. - -In the new setup that I want, a user should be able to specicfy, directly in the test suite, as which organization it wants to submit a transaction, by specifying the "name" or the organization. The software should then fetch the details of the corresponding wallet/connection profile to submit the transaction. Also, when referring to public keys in the testsuite, again, specifying the name of the organization instead would be much easier and repetable. - -The new version will work alongside another software, called easycc. It supports the creation and deployment of HLF networks and chaincodes, and exports credentials for multiple organizations. You can find an example of folder that contains a workspace of easycc, with details on the credentials/wallet, etc. in the directories, and which includes multiple organizations. If I create a network with two organizations (say "supplier" and "provider"), I should be able to say, for each transaction in the test suite, whether I want to run it as supplier or provider, and be able to specify the "supplier's public key" by mentioning "supplier" instead of the actual public key (same for all types of participants). The type of participant / name of organization is not pre-defined but is user-defined when the network is created and the test suite is written. - -Here's an example of how to run the program current version: ./ChaincoderTestr --wallet-path /Users/vamarald/Dev/testn/exports/wallets/org1 --identity-name "Org1" --connection-profile-path /Users/vamarald/Dev/testn/exports/connection-profiles/connection-org1.json --chaincode-name chaincode --test-suite-path /Users/vamarald/Dev/ChaincodeTestr/src/test/resources/happy-path-test-suite.json - -Here's how I want to be able to run the new version, with the other changes above: -./Chaincodetestr --worskspace ROOT_OF_EASYCC_WORKSPACE ---test-suite-path PATH_TO_TEST_SUITE ---init-as NAME_OF_INITIALIZING_ORGANIZATION ---chaincode-name CC_NAME ---chaincode-version VERSION - -You can look in-depth in: -~/Dev/testn: to see the content of a easycc sample workspace -~/Dev/easycc: to see the full source code of easycc, to know how it maps organization names to outputs in different ways (wallets, connection profiles, etc.). - -Also, keep a markdown file with the original README (for version 0.3), but create a new, update README.md that will include updated documentation, details on the changes from v0.3 to v1.0 (the version we are going to implement), and a link to the older version's readme. - -Before you make any changes, make a thorough review of the concerned softwares, and propose me a comprehensive plan for the implementation of the changes that are required. If you have any question or clarification required, ask it now if possible, but later questions are of course allowed. From 9bd1d08bfb9a232b32ba4eb7b606636b7132bbec Mon Sep 17 00:00:00 2001 From: "Victor Amaral de Sousa, PhD" Date: Sat, 14 Feb 2026 09:34:11 +0100 Subject: [PATCH 4/4] log wallet used --- src/main/kotlin/be/vamaralds/lib/ContractHandler.kt | 4 ++-- .../kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt b/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt index e435073..dc4e5c8 100644 --- a/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt +++ b/src/main/kotlin/be/vamaralds/lib/ContractHandler.kt @@ -126,10 +126,10 @@ class ContractHandler(private val contract: Contract) { @Throws(ContractTransactionException::class) fun submitTransaction(transactionName: String, vararg args: String): String { - ChaincodeApplication.logger.debug { "Submitting transaction $transactionName with args: ${args.joinToString()}" } + ChaincodeApplication.logger.info { "Submitting transaction $transactionName with args: ${args.joinToString()}" } try { val result = contract.submitTransaction(transactionName, *args).toString(Charsets.UTF_8) - ChaincodeApplication.logger.debug { "Transaction $transactionName submitted successfully - Result: $result" } + ChaincodeApplication.logger.info { "Transaction $transactionName submitted successfully - Result: $result" } return result } catch(e: Exception) { when(e) { diff --git a/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt b/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt index bb180c4..78a66a8 100644 --- a/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt +++ b/src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt @@ -24,7 +24,9 @@ class MultiOrgConnectionManager( } // Create new connection - ChaincodeApplication.logger.debug { "Creating connection for organization: $orgName" } + ChaincodeApplication.logger.info { "Creating connection for organization: $orgName" } + ChaincodeApplication.logger.info { " Wallet: ${workspaceManager.getWalletPath(orgName)}" } + ChaincodeApplication.logger.info { " Identity: ${workspaceManager.getIdentityName(orgName)}" } val config = ConnectionConfiguration( walletPath = workspaceManager.getWalletPath(orgName), @@ -37,7 +39,7 @@ class MultiOrgConnectionManager( val connection = ChaincodeTestr(config).connect() connections[orgName] = connection - ChaincodeApplication.logger.debug { "Successfully created connection for organization: $orgName" } + ChaincodeApplication.logger.info { "Successfully created connection for organization: $orgName" } return connection }