317 lines
11 KiB
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")
|
|
}
|