diff --git a/internal/blockchain/ethereum/quorum/client_test.go b/internal/blockchain/ethereum/quorum/client_test.go new file mode 100644 index 0000000..11e42ce --- /dev/null +++ b/internal/blockchain/ethereum/quorum/client_test.go @@ -0,0 +1,81 @@ +package quorum + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-cli/internal/utils" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestUnlockAccount(t *testing.T) { + tests := []struct { + Name string + RPCUrl string + Address string + Password string + StatusCode int + ApiResponse *JSONRPCResponse + }{ + { + Name: "TestUnlockAccount-1", + RPCUrl: "http://127.0.0.1:8545", + Address: "user-1", + Password: "POST", + StatusCode: 200, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: nil, + Result: "mock result", + }, + }, + { + Name: "TestUnlockAccountError-2", + RPCUrl: "http://127.0.0.1:8545", + Address: "user-1", + Password: "POST", + StatusCode: 200, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: &JSONRPCError{500, "invalid account"}, + Result: "mock result", + }, + }, + { + Name: "TestUnlockAccountHTTPError-3", + RPCUrl: "http://localhost:8545", + Address: "user-1", + Password: "POST", + StatusCode: 500, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: nil, + Result: "mock result", + }, + }, + } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + apiResponse, _ := json.Marshal(tc.ApiResponse) + // mockResponse + httpmock.RegisterResponder("POST", tc.RPCUrl, + httpmock.NewStringResponder(tc.StatusCode, string(apiResponse))) + client := NewQuorumClient(tc.RPCUrl) + utils.StartMockServer(t) + err := client.UnlockAccount(tc.Address, tc.Password) + utils.StopMockServer(t) + + // expect errors when returned status code != 200 or ApiResponse comes back with non nil error + if tc.StatusCode != 200 || tc.ApiResponse.Error != nil { + assert.NotNil(t, err, "expects error to be returned when either quorum returns an application error or non 200 http response") + } else { + assert.NoError(t, err, fmt.Sprintf("unable to unlock account: %v", err)) + } + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go b/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go new file mode 100644 index 0000000..938f760 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go @@ -0,0 +1,112 @@ +package quorum + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestCreateTesseraKeys(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + TesseraImage string + KeysPrefix string + KeysName string + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + TesseraImage: "quorumengineering/tessera:24.4", + KeysPrefix: "", + KeysName: "tm", + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + TesseraImage: "quorumengineering/tessera:24.4", + KeysPrefix: "xyz", + KeysName: "tm", + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + privateKey, publicKey, tesseraKeysPath, err := CreateTesseraKeys(ctx, tc.TesseraImage, filepath.Join(tc.Stack.InitDir, "tessera", "tessera_0", "keystore"), tc.KeysPrefix, tc.KeysName) + if err != nil { + t.Log("unable to create tessera keys", err) + } + //validate properties of tessera keys + assert.NotEmpty(t, privateKey) + assert.NotEmpty(t, publicKey) + assert.NotEmpty(t, tesseraKeysPath) + + expectedOutputName := tc.KeysName + if tc.KeysPrefix != "" { + expectedOutputName = fmt.Sprintf("%s_%s", tc.KeysPrefix, expectedOutputName) + } + assert.Equal(t, tesseraKeysPath, filepath.Join(tc.Stack.InitDir, "tessera", "tessera_0", "keystore", expectedOutputName), "invalid output path") + + assert.Nil(t, err) + }) + } +} + +func TestCreateTesseraEntrypoint(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + StackName string + MemberCount int + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + StackName: "org1", + MemberCount: 4, + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-2_quorum", + InitDir: t.TempDir(), + }, + StackName: "org2", + MemberCount: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + err := CreateTesseraEntrypoint(ctx, tc.Stack.InitDir, tc.StackName, tc.MemberCount) + if err != nil { + t.Log("unable to create tessera docker entrypoint", err) + } + path := filepath.Join(tc.Stack.InitDir, "docker-entrypoint.sh") + _, err = os.Stat(path) + assert.NoError(t, err, "docker entrypoint file not created") + + b, err := os.ReadFile(path) + assert.NoError(t, err, "unable to read docker entrypoint file") + for i := 0; i < tc.MemberCount; i++ { + strings.Contains(string(b), fmt.Sprintf("member%dtessera", i)) + } + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/quorum_provider.go b/internal/blockchain/ethereum/quorum/quorum_provider.go index fb8b701..3a0c878 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider.go @@ -38,7 +38,7 @@ import ( var quorumImage = "quorumengineering/quorum:24.4" var tesseraImage = "quorumengineering/tessera:24.4" -var exposedBlockchainPortMultiplier = 10 +var ExposedBlockchainPortMultiplier = 10 // TODO: Probably randomize this and make it different per member? var keyPassword = "correcthorsebatterystaple" @@ -47,6 +47,7 @@ type QuorumProvider struct { ctx context.Context stack *types.Stack connector connector.Connector + dockerMgr docker.IDockerManager } func NewQuorumProvider(ctx context.Context, stack *types.Stack) *QuorumProvider { @@ -62,6 +63,7 @@ func NewQuorumProvider(ctx context.Context, stack *types.Stack) *QuorumProvider ctx: ctx, stack: stack, connector: connector, + dockerMgr: docker.NewDockerManager(), } } @@ -125,7 +127,7 @@ func (p *QuorumProvider) FirstTimeSetup() error { // Copy connector config to each member's volume connectorConfigPath := filepath.Join(p.stack.StackDir, "runtime", "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) connectorConfigVolumeName := fmt.Sprintf("%s_%s_config_%v", p.stack.Name, p.connector.Name(), i) - if err := docker.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { return err } @@ -135,39 +137,39 @@ func (p *QuorumProvider) FirstTimeSetup() error { // Copy the wallet files of each member to their respective blockchain volume keystoreDirectory := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, keystoreDirectory, "/"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, keystoreDirectory, "/"); err != nil { return err } if p.stack.TesseraEnabled { // Copy member specific tessera key files - if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { + if err := p.dockerMgr.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { return err } tmKeystoreDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmKeystoreDirectory, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmKeystoreDirectory, rootDir); err != nil { return err } // Copy tessera docker-entrypoint file tmEntrypointPath := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), DockerEntrypoint) - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmEntrypointPath, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmEntrypointPath, rootDir); err != nil { return err } } // Copy quorum docker-entrypoint file quorumEntrypointPath := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), DockerEntrypoint) - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, quorumEntrypointPath, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, quorumEntrypointPath, rootDir); err != nil { return err } // Copy the genesis block information - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { return err } // Initialize the genesis block - if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", quorumVolumeNameMember), quorumImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { + if err := p.dockerMgr.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", quorumVolumeNameMember), quorumImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { return err } } @@ -190,6 +192,7 @@ func (p *QuorumProvider) PostStart(firstTimeSetup bool) error { for _, member := range p.stack.Members { if member.Account.(*ethereum.Account).Address == address { memberIndex = *member.Index + break } } if err := p.unlockAccount(address, keyPassword, memberIndex); err != nil { @@ -204,7 +207,7 @@ func (p *QuorumProvider) unlockAccount(address, password string, memberIndex int l := log.LoggerFromContext(p.ctx) verbose := log.VerbosityFromContext(p.ctx) // exposed blockchain port is the default for node 0, we need to add the port multiplier to get the right rpc for the correct node - quorumClient := NewQuorumClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*exposedBlockchainPortMultiplier))) + quorumClient := NewQuorumClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*ExposedBlockchainPortMultiplier))) retries := 10 for { if err := quorumClient.UnlockAccount(address, password); err != nil { @@ -250,7 +253,7 @@ func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefiniti ContainerName: fmt.Sprintf("%s_member%dtessera", p.stack.Name, i), Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 + Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*ExposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, @@ -265,7 +268,7 @@ func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefiniti ContainerName: fmt.Sprintf("%s_quorum_%d", p.stack.Name, i), Volumes: []string{fmt.Sprintf("quorum_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*ExposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, DependsOn: quorumDependsOn, diff --git a/internal/blockchain/ethereum/quorum/quorum_provider_test.go b/internal/blockchain/ethereum/quorum/quorum_provider_test.go index 3428282..d67461d 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider_test.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider_test.go @@ -3,15 +3,19 @@ package quorum import ( "context" "encoding/hex" + "fmt" "os" "path/filepath" "testing" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum" + "github.com/hyperledger/firefly-cli/internal/docker/mocks" "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/internal/utils" "github.com/hyperledger/firefly-cli/pkg/types" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) @@ -46,8 +50,8 @@ func TestNewQuorumProvider(t *testing.T) { }, }, }, - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Evmconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "evmconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, @@ -66,8 +70,8 @@ func TestNewQuorumProvider(t *testing.T) { }, }, }, - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Ethconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, @@ -285,40 +289,57 @@ func TestGetConnectorExternal(t *testing.T) { func TestCreateAccount(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) - testcases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - Args []string + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string }{ { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), + TesseraEnabled: false, }, - Args: []string{"Org-1_quorum", "Org-1_quorum", "0", "1"}, + Args: []string{"Org-1_quorum", "Org-1_quorum", "0"}, }, { - Name: "testcase1", + Name: "testcase2", Ctx: ctx, Stack: &types.Stack{ Name: "Org-2_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + }, + Args: []string{"Org-2_quorum", "Org-2_quorum", "1"}, + }, + { + Name: "testcase3", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-3_quorum", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "EvmConnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), + TesseraEnabled: true, }, - Args: []string{"Org-2_quorum", "Org-2_quorum", "1", "2"}, + Args: []string{"Org-3_quorum", "Org-3_quorum", "1"}, }, } - for _, tc := range testcases { + for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { p := NewQuorumProvider(tc.Ctx, tc.Stack) Account, err := p.CreateAccount(tc.Args) @@ -345,3 +366,140 @@ func TestCreateAccount(t *testing.T) { }) } } + +func TestPostStart(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string + }{ + { + Name: "testcase1", + Ctx: ctx, + Stack: &types.Stack{ + State: &types.StackState{ + DeployedContracts: make([]*types.DeployedContract, 0), + }, + ExposedBlockchainPort: 8545, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Index: &[]int{1}[0], + Account: ðereum.Account{ + Address: "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", + PrivateKey: "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + accounts := make([]interface{}, len(tc.Stack.Members)) + for memberIndex, member := range tc.Stack.Members { + accounts[memberIndex] = member.Account + + } + tc.Stack.State.Accounts = accounts + p := NewQuorumProvider(tc.Ctx, tc.Stack) + utils.StartMockServer(t) + // mock quorum rpc response during the unlocking of accounts + for _, member := range tc.Stack.Members { + rpcUrl := fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(*member.Index*ExposedBlockchainPortMultiplier)) + httpmock.RegisterResponder( + "POST", + rpcUrl, + httpmock.NewStringResponder(200, "{\"JSONRPC\": \"2.0\"}")) + } + httpmock.Activate() + hasRunBefore, _ := p.stack.HasRunBefore() + err := p.PostStart(hasRunBefore) + assert.Nil(t, err, "post start should not have an error") + utils.StopMockServer(t) + }) + } +} + +func TestFirstTimeSetup(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string + }{ + { + Name: "testcase1_no_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + }, + }, + { + Name: "testcase2_with_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + }, + { + Index: &[]int{1}[0], + }, + }, + }, + }, + { + Name: "testcase3_with_members_and_tessera_enabled", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: true, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + p := NewQuorumProvider(tc.Ctx, tc.Stack) + p.dockerMgr = mocks.NewDockerManager() // docker related functionality should be tested in docker package + error := p.FirstTimeSetup() + assert.Nil(t, error, "first time setup should not throw an error") + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/quorum_test.go b/internal/blockchain/ethereum/quorum/quorum_test.go new file mode 100644 index 0000000..e195a29 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/quorum_test.go @@ -0,0 +1,72 @@ +package quorum + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestCreateQuorumEntrypoint(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + Consensus string + StackName string + MemberIndex int + ChainID int + BlockPeriodInSeconds int + TesseraEnabled bool + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + Consensus: "ibft", + StackName: "org1", + MemberIndex: 0, + ChainID: 1337, + BlockPeriodInSeconds: -1, + TesseraEnabled: true, + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-2_quorum", + InitDir: t.TempDir(), + }, + Consensus: "clique", + StackName: "org2", + MemberIndex: 1, + ChainID: 1337, + BlockPeriodInSeconds: 3, + TesseraEnabled: false, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + err := CreateQuorumEntrypoint(ctx, tc.Stack.InitDir, tc.Consensus, tc.StackName, tc.MemberIndex, tc.ChainID, tc.BlockPeriodInSeconds, tc.TesseraEnabled) + if err != nil { + t.Log("unable to create quorum docker entrypoint", err) + } + path := filepath.Join(tc.Stack.InitDir, "docker-entrypoint.sh") + _, err = os.Stat(path) + assert.NoError(t, err, "docker entrypoint file not created") + + b, err := os.ReadFile(path) + assert.NoError(t, err, "unable to read docker entrypoint file") + output := string(b) + strings.Contains(output, fmt.Sprintf("member%dtessera", tc.MemberIndex)) + strings.Contains(output, fmt.Sprintf("GOQUORUM_CONS_ALGO=%s", tc.Consensus)) + }) + } +} diff --git a/internal/docker/docker_manager.go b/internal/docker/docker_manager.go new file mode 100644 index 0000000..ae21ce2 --- /dev/null +++ b/internal/docker/docker_manager.go @@ -0,0 +1,104 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker + +import ( + "context" +) + +// DockerInterface combines all Docker-related operations into a single interface. +type IDockerManager interface { + // Command Execution + RunDockerCommand(ctx context.Context, workingDir string, command ...string) error + RunDockerCommandLine(ctx context.Context, workingDir string, command string) error + RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error + RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) + RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) + + // Image Inspection + GetImageConfig(image string) (map[string]interface{}, error) + GetImageLabel(image, label string) (string, error) + GetImageDigest(image string) (string, error) + + // Volume Management + CreateVolume(ctx context.Context, volumeName string) error + CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error + MkdirInVolume(ctx context.Context, volumeName string, directory string) error + RemoveVolume(ctx context.Context, volumeName string) error + + // Container Interaction + CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error +} + +// DockerManager implements IDockerManager +type DockerManager struct{} + +func NewDockerManager() *DockerManager { + return &DockerManager{} +} + +func (mgr *DockerManager) RunDockerCommand(ctx context.Context, workingDir string, command ...string) error { + return RunDockerCommand(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerCommandLine(ctx context.Context, workingDir string, command string) error { + return RunDockerCommandLine(ctx, workingDir, command) +} + +func (mgr *DockerManager) RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error { + return RunDockerComposeCommand(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) { + return RunDockerCommandBuffered(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) { + return RunDockerComposeCommandReturnsStdout(workingDir, command...) +} + +func (mgr *DockerManager) GetImageConfig(image string) (map[string]interface{}, error) { + return GetImageConfig(image) +} + +func (mgr *DockerManager) GetImageLabel(image, label string) (string, error) { + return GetImageLabel(image, label) +} + +func (mgr *DockerManager) GetImageDigest(image string) (string, error) { + return GetImageDigest(image) +} + +func (mgr *DockerManager) CreateVolume(ctx context.Context, volumeName string) error { + return CreateVolume(ctx, volumeName) +} + +func (mgr *DockerManager) CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error { + return CopyFileToVolume(ctx, volumeName, sourcePath, destPath) +} + +func (mgr *DockerManager) MkdirInVolume(ctx context.Context, volumeName string, directory string) error { + return MkdirInVolume(ctx, volumeName, directory) +} + +func (mgr *DockerManager) RemoveVolume(ctx context.Context, volumeName string) error { + return RemoveVolume(ctx, volumeName) +} + +func (mgr *DockerManager) CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error { + return CopyFromContainer(ctx, containerName, sourcePath, destPath) +} diff --git a/internal/docker/mocks/docker_manager.go b/internal/docker/mocks/docker_manager.go new file mode 100644 index 0000000..982a8d9 --- /dev/null +++ b/internal/docker/mocks/docker_manager.go @@ -0,0 +1,62 @@ +// DockerManager is a mock that implements IDockerManager +package mocks + +import "context" + +type DockerManager struct{} + +func NewDockerManager() *DockerManager { + return &DockerManager{} +} + +func (mgr *DockerManager) RunDockerCommand(ctx context.Context, workingDir string, command ...string) error { + return nil +} + +func (mgr *DockerManager) RunDockerCommandLine(ctx context.Context, workingDir string, command string) error { + return nil +} + +func (mgr *DockerManager) RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error { + return nil +} + +func (mgr *DockerManager) RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) { + return nil, nil +} + +func (mgr *DockerManager) GetImageConfig(image string) (map[string]interface{}, error) { + return nil, nil +} + +func (mgr *DockerManager) GetImageLabel(image, label string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) GetImageDigest(image string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) CreateVolume(ctx context.Context, volumeName string) error { + return nil +} + +func (mgr *DockerManager) CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error { + return nil +} + +func (mgr *DockerManager) MkdirInVolume(ctx context.Context, volumeName string, directory string) error { + return nil +} + +func (mgr *DockerManager) RemoveVolume(ctx context.Context, volumeName string) error { + return nil +} + +func (mgr *DockerManager) CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error { + return nil +}