go-blockchain/commands_test.go

317 lines
11 KiB
Go

// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package blockchain
import (
"context"
"errors"
"net"
"testing"
"time"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddChainCommands_Good_RegistersParent(t *testing.T) {
root := &cobra.Command{Use: "test"}
AddChainCommands(root)
chainCmd, _, err := root.Find([]string{ChainCommandName})
require.NoError(t, err)
assert.Equal(t, ChainCommandName, chainCmd.Name())
assert.Equal(t, chainCommandShort, chainCmd.Short)
assert.Equal(t, chainCommandLong, chainCmd.Long)
assert.Equal(t, chainCommandExample, chainCmd.Example)
}
func TestAddChainCommands_Good_HasSubcommands(t *testing.T) {
root := &cobra.Command{Use: "test"}
AddChainCommands(root)
chainCmd, _, _ := root.Find([]string{ChainCommandName})
var names []string
for _, sub := range chainCmd.Commands() {
names = append(names, sub.Name())
}
assert.Contains(t, names, chainExplorerCommandName)
assert.Contains(t, names, chainSyncCommandName)
}
func TestAddChainCommands_Good_PersistentFlags(t *testing.T) {
root := &cobra.Command{Use: "test"}
AddChainCommands(root)
chainCmd, _, _ := root.Find([]string{ChainCommandName})
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("data-dir"))
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed"))
assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet"))
}
func TestNewChainExplorerCommand_Good_Metadata(t *testing.T) {
dataDir := ""
seed := ""
testnet := false
cmd := newChainExplorerCommand(&dataDir, &seed, &testnet)
assert.Equal(t, chainExplorerCommandName, cmd.Use)
assert.Equal(t, chainExplorerCommandShort, cmd.Short)
assert.Equal(t, chainExplorerCommandLong, cmd.Long)
assert.Equal(t, chainExplorerExample, cmd.Example)
assert.Equal(t, "Chain Explorer", chainExplorerFrameTitle)
assert.ErrorContains(t, cmd.Args(cmd, []string{"unexpected"}), "unknown command")
}
func TestNewChainSyncCommand_Good_Metadata(t *testing.T) {
dataDir := ""
seed := ""
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
assert.Equal(t, chainSyncCommandName, cmd.Use)
assert.Equal(t, chainSyncCommandShort, cmd.Short)
assert.Equal(t, chainSyncCommandLong, cmd.Long)
assert.Equal(t, chainSyncExample, cmd.Example)
assert.ErrorContains(t, cmd.Args(cmd, []string{"unexpected"}), "unknown command")
}
func TestAppName_Good(t *testing.T) {
assert.Equal(t, "core-chain", AppName)
}
func TestChainCommandName_Good(t *testing.T) {
assert.Equal(t, "chain", ChainCommandName)
}
func TestChainCommandLabels_Good(t *testing.T) {
assert.Equal(t, "Sync, inspect, and manage the blockchain", chainCommandShort)
assert.Equal(t, "Sync from peers, inspect the chain in a terminal UI, or manage a background sync.", chainCommandLong)
assert.Equal(t, "Open the chain explorer", chainExplorerCommandShort)
assert.Equal(t, "Browse blocks and transactions in a terminal UI while the node keeps syncing.", chainExplorerCommandLong)
assert.Equal(t, "Start or stop chain sync", chainSyncCommandShort)
assert.Equal(t, "Start syncing from peers in the foreground by default. Use --daemon to keep syncing in the background, or --stop to stop the background sync for the selected data directory.", chainSyncCommandLong)
}
func TestValidateChainRuntimeInputs_Good(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "127.0.0.1:36942")
require.NoError(t, err)
}
func TestValidateChainSyncModeSelection_Good(t *testing.T) {
err := validateChainSyncModeSelection(true, false)
require.NoError(t, err)
}
func TestAddChainCommands_Bad_RejectsUnexpectedArgs(t *testing.T) {
root := &cobra.Command{Use: "test"}
AddChainCommands(root)
chainCmd, _, err := root.Find([]string{ChainCommandName})
require.NoError(t, err)
require.ErrorContains(t, chainCmd.Args(chainCmd, []string{"unexpected"}), "unknown command")
}
func TestNewChainSyncCommand_Bad_RejectsConflictingFlags(t *testing.T) {
dataDir := ""
seed := ""
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
require.NoError(t, cmd.Flags().Set("daemon", "true"))
require.NoError(t, cmd.Flags().Set("stop", "true"))
err := cmd.RunE(cmd, nil)
require.Error(t, err)
assert.EqualError(t, err, "blockchain: choose either --daemon to start background sync or --stop to stop it")
}
func TestNewChainSyncCommand_Bad_RejectsEmptyDataDirOnStop(t *testing.T) {
dataDir := ""
seed := ""
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
require.NoError(t, cmd.Flags().Set("stop", "true"))
err := cmd.RunE(cmd, nil)
require.Error(t, err)
assert.EqualError(t, err, "blockchain: --data-dir must not be empty")
}
func TestValidateChainRuntimeInputs_Bad_RejectsEmptyDataDir(t *testing.T) {
err := validateChainRuntimeInputs("", "127.0.0.1:36942")
require.EqualError(t, err, "blockchain: --data-dir must not be empty")
}
func TestValidateChainRuntimeInputs_Bad_RejectsEmptySeed(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "")
require.EqualError(t, err, "blockchain: --seed must not be empty")
}
func TestValidateChainRuntimeInputs_Bad_RejectsMalformedSeed(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "seeds.lthn.io")
require.EqualError(t, err, `blockchain: invalid --seed "seeds.lthn.io": expected host:port`)
}
func TestValidateChainRuntimeInputs_Bad_RejectsNonNumericSeedPort(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "seeds.lthn.io:http")
require.EqualError(t, err, `blockchain: invalid --seed "seeds.lthn.io:http": expected host:port with a numeric port`)
}
func TestValidateChainRuntimeInputs_Bad_RejectsOutOfRangeSeedPort(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "seeds.lthn.io:65536")
require.EqualError(t, err, `blockchain: invalid --seed "seeds.lthn.io:65536": expected host:port with a port from 1 to 65535`)
}
func TestValidateChainRuntimeInputs_Bad_RejectsZeroSeedPort(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "seeds.lthn.io:0")
require.EqualError(t, err, `blockchain: invalid --seed "seeds.lthn.io:0": expected host:port with a port from 1 to 65535`)
}
func TestValidateChainRuntimeInputs_Bad_RejectsNegativeSeedPort(t *testing.T) {
err := validateChainRuntimeInputs("/tmp/lethean", "seeds.lthn.io:-1")
require.EqualError(t, err, `blockchain: invalid --seed "seeds.lthn.io:-1": expected host:port with a port from 1 to 65535`)
}
func TestValidateChainSyncModeSelection_Bad_RejectsConflictingFlags(t *testing.T) {
err := validateChainSyncModeSelection(true, true)
require.EqualError(t, err, "blockchain: choose either --daemon to start background sync or --stop to stop it")
}
func TestAddChainCommands_Good_PersistentFlagHelp(t *testing.T) {
root := &cobra.Command{Use: "test"}
AddChainCommands(root)
chainCmd, _, _ := root.Find([]string{ChainCommandName})
assert.Equal(t, "store chain data and sync state in this directory", chainCmd.PersistentFlags().Lookup("data-dir").Usage)
assert.Equal(t, "connect to this seed peer first (host:port)", chainCmd.PersistentFlags().Lookup("seed").Usage)
assert.Equal(t, "use the Lethean testnet and its default seed (localhost:46942)", chainCmd.PersistentFlags().Lookup("testnet").Usage)
}
func TestNewChainSyncCommand_Good_FlagHelp(t *testing.T) {
dataDir := ""
seed := ""
testnet := false
cmd := newChainSyncCommand(&dataDir, &seed, &testnet)
assert.Equal(t, "keep syncing in the background", cmd.Flags().Lookup("daemon").Usage)
assert.Equal(t, "stop the background sync for this data directory", cmd.Flags().Lookup("stop").Usage)
}
func TestChainSyncExamples_Good(t *testing.T) {
assert.Contains(t, chainSyncExample, "core-chain chain sync --daemon")
assert.Contains(t, chainSyncExample, "core-chain chain sync --stop --data-dir ~/.lethean/chain")
}
func TestFormatChainSyncForegroundStart_Good(t *testing.T) {
assert.Equal(t, `Starting chain sync in the foreground with data dir "/tmp/chain" and seed "seeds.lthn.io:36942". Press Ctrl+C to stop.`, formatChainSyncForegroundStart("/tmp/chain", "seeds.lthn.io:36942"))
}
func TestFormatChainSyncForegroundStop_Good(t *testing.T) {
assert.Equal(t, "Chain sync stopped.", formatChainSyncForegroundStop())
}
func TestFormatChainSyncDaemonStarted_Good(t *testing.T) {
assert.Equal(t, `Background chain sync started with data dir "/tmp/chain" and seed "seeds.lthn.io:36942". Stop it with core-chain chain sync --stop --data-dir "/tmp/chain".`, formatChainSyncDaemonStarted("/tmp/chain", "seeds.lthn.io:36942"))
}
func TestFormatChainSyncStopCommand_Good(t *testing.T) {
assert.Equal(t, `core-chain chain sync --stop --data-dir "/tmp/chain"`, formatChainSyncStopCommand("/tmp/chain"))
}
func TestFormatChainSyncDaemonStopping_Good(t *testing.T) {
assert.Equal(t, "Stopping background chain sync (PID 42).", formatChainSyncDaemonStopping(42))
}
func TestRunChainSyncOnce_Bad_RespectsCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
start := time.Now()
err := runChainSyncOnce(ctx, nil, &config.ChainConfig{}, chain.SyncOptions{}, "198.51.100.1:65535")
require.Error(t, err)
assert.Less(t, time.Since(start), 2*time.Second)
}
func TestRunChainSyncOnce_Bad_ReportsPeerIDReadError(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
done := make(chan struct{})
defer close(done)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
<-done
}()
oldReadPeerID := readPeerID
readPeerID = func([]byte) (int, error) {
return 0, errors.New("peer id failed")
}
defer func() {
readPeerID = oldReadPeerID
}()
err = runChainSyncOnce(context.Background(), nil, &config.ChainConfig{}, chain.SyncOptions{}, listener.Addr().String())
require.Error(t, err)
assert.ErrorContains(t, err, "read peer ID")
assert.ErrorContains(t, err, "peer id failed")
}
func TestRunChainSyncOnce_Bad_ReportsHeightLookupError(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
done := make(chan struct{})
defer close(done)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
<-done
}()
oldReadPeerID := readPeerID
readPeerID = func(buf []byte) (int, error) {
copy(buf, []byte{1, 2, 3, 4, 5, 6, 7, 8})
return len(buf), nil
}
defer func() {
readPeerID = oldReadPeerID
}()
oldChainHeight := chainHeight
chainHeight = func(*chain.Chain) (uint64, error) {
return 0, errors.New("height failed")
}
defer func() {
chainHeight = oldChainHeight
}()
err = runChainSyncOnce(context.Background(), nil, &config.ChainConfig{}, chain.SyncOptions{}, listener.Addr().String())
require.Error(t, err)
assert.ErrorContains(t, err, "read local height")
assert.ErrorContains(t, err, "height failed")
}