github.com/cosmos/cosmos-sdk@v0.50.10/server/export_test.go (about) 1 package server_test 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "testing" 11 "time" 12 13 cmtcfg "github.com/cometbft/cometbft/config" 14 cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" 15 cmttypes "github.com/cometbft/cometbft/types" 16 dbm "github.com/cosmos/cosmos-db" 17 "github.com/rs/zerolog" 18 "github.com/spf13/viper" 19 "github.com/stretchr/testify/require" 20 21 "cosmossdk.io/log" 22 23 "github.com/cosmos/cosmos-sdk/client" 24 "github.com/cosmos/cosmos-sdk/server" 25 "github.com/cosmos/cosmos-sdk/server/types" 26 "github.com/cosmos/cosmos-sdk/testutil/cmdtest" 27 "github.com/cosmos/cosmos-sdk/types/module" 28 genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" 29 genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" 30 ) 31 32 // ExportSystem wraps a (*cmdtest).System 33 // and sets up appropriate client and server contexts, 34 // to simplify testing the export CLI. 35 type ExportSystem struct { 36 sys *cmdtest.System 37 38 Ctx context.Context 39 40 Sctx *server.Context 41 Cctx client.Context 42 43 HomeDir string 44 } 45 46 // newExportSystem returns a cmdtest.System with export as a child command, 47 // and it returns a context.Background with an associated *server.Context value. 48 func NewExportSystem(t *testing.T, exporter types.AppExporter) *ExportSystem { 49 t.Helper() 50 51 homeDir := t.TempDir() 52 53 // Unclear why we have to create the config directory ourselves, 54 // but tests fail without this. 55 if err := os.MkdirAll(filepath.Join(homeDir, "config"), 0o700); err != nil { 56 t.Fatal(err) 57 } 58 59 sys := cmdtest.NewSystem() 60 sys.AddCommands( 61 server.ExportCmd(exporter, homeDir), 62 genutilcli.InitCmd(module.NewBasicManager(), homeDir), 63 ) 64 65 tw := zerolog.NewTestWriter(t) 66 tw.Frame = 5 // Seems to be the magic number to get source location to match logger calls. 67 68 sCtx := server.NewContext( 69 viper.New(), 70 cmtcfg.DefaultConfig(), 71 log.NewCustomLogger(zerolog.New(tw)), 72 ) 73 sCtx.Config.SetRoot(homeDir) 74 75 cCtx := (client.Context{}).WithHomeDir(homeDir) 76 77 ctx := context.WithValue(context.Background(), server.ServerContextKey, sCtx) 78 ctx = context.WithValue(ctx, client.ClientContextKey, &cCtx) 79 80 return &ExportSystem{ 81 sys: sys, 82 Ctx: ctx, 83 Sctx: sCtx, 84 Cctx: cCtx, 85 HomeDir: homeDir, 86 } 87 } 88 89 // Run wraps (*cmdtest.System).RunC, providing e's context. 90 func (s *ExportSystem) Run(args ...string) cmdtest.RunResult { 91 return s.sys.RunC(s.Ctx, args...) 92 } 93 94 // MustRun wraps (*cmdtest.System).MustRunC, providing e's context. 95 func (s *ExportSystem) MustRun(t *testing.T, args ...string) cmdtest.RunResult { 96 return s.sys.MustRunC(t, s.Ctx, args...) 97 } 98 99 // isZeroExportedApp reports whether all fields of a are unset. 100 // 101 // This is for the mockExporter to check if a return value was ever set. 102 func isZeroExportedApp(a types.ExportedApp) bool { 103 return a.AppState == nil && 104 len(a.Validators) == 0 && 105 a.Height == 0 && 106 a.ConsensusParams == cmtproto.ConsensusParams{} 107 } 108 109 // mockExporter provides an Export method matching server/types.AppExporter, 110 // and it tracks relevant arguments when that method is called. 111 type mockExporter struct { 112 // The values to return from Export(). 113 ExportApp types.ExportedApp 114 Err error 115 116 // Whether Export was called at all. 117 WasCalled bool 118 119 // Called tracks the interesting arguments passed to Export(). 120 Called struct { 121 Height int64 122 ForZeroHeight bool 123 JailAllowedAddrs []string 124 ModulesToExport []string 125 } 126 } 127 128 // SetDefaultExportApp sets a valid ExportedApp to be returned 129 // when e.Export is called. 130 func (e *mockExporter) SetDefaultExportApp() { 131 e.ExportApp = types.ExportedApp{ 132 ConsensusParams: cmtproto.ConsensusParams{ 133 Block: &cmtproto.BlockParams{ 134 MaxBytes: 5 * 1024 * 1024, 135 MaxGas: -1, 136 }, 137 Evidence: &cmtproto.EvidenceParams{ 138 MaxAgeNumBlocks: 100, 139 MaxAgeDuration: time.Hour, 140 MaxBytes: 1024 * 1024, 141 }, 142 Validator: &cmtproto.ValidatorParams{ 143 PubKeyTypes: []string{cmttypes.ABCIPubKeyTypeEd25519}, 144 }, 145 }, 146 } 147 } 148 149 // Export satisfies the server/types.AppExporter function type. 150 // 151 // e tracks relevant arguments under the e.Called struct. 152 // 153 // Export panics if neither e.ExportApp nor e.Err have been set. 154 func (e *mockExporter) Export( 155 logger log.Logger, 156 db dbm.DB, 157 traceWriter io.Writer, 158 height int64, 159 forZeroHeight bool, 160 jailAllowedAddrs []string, 161 opts types.AppOptions, 162 modulesToExport []string, 163 ) (types.ExportedApp, error) { 164 if e.Err == nil && isZeroExportedApp(e.ExportApp) { 165 panic(fmt.Errorf("(*mockExporter).Export called without setting e.ExportApp or e.Err")) 166 } 167 e.WasCalled = true 168 169 e.Called.Height = height 170 e.Called.ForZeroHeight = forZeroHeight 171 e.Called.JailAllowedAddrs = jailAllowedAddrs 172 e.Called.ModulesToExport = modulesToExport 173 174 return e.ExportApp, e.Err 175 } 176 177 func TestExportCLI(t *testing.T) { 178 // Use t.Parallel in all of the subtests, 179 // because they all read from disk and risk blocking on io. 180 181 t.Run("fail on missing genesis file", func(t *testing.T) { 182 t.Parallel() 183 184 e := new(mockExporter) 185 sys := NewExportSystem(t, e.Export) 186 187 res := sys.Run("export") 188 require.Error(t, res.Err) 189 require.Truef(t, os.IsNotExist(res.Err), "expected resulting error to be os.IsNotExist, got %T (%v)", res.Err, res.Err) 190 191 require.False(t, e.WasCalled) 192 }) 193 194 t.Run("prints to stdout by default", func(t *testing.T) { 195 t.Parallel() 196 197 e := new(mockExporter) 198 e.SetDefaultExportApp() 199 200 sys := NewExportSystem(t, e.Export) 201 _ = sys.MustRun(t, "init", "some_moniker") 202 res := sys.MustRun(t, "export") 203 204 require.Empty(t, res.Stderr.String()) 205 206 CheckExportedGenesis(t, res.Stdout.Bytes()) 207 }) 208 209 t.Run("passes expected default values to the AppExporter", func(t *testing.T) { 210 t.Parallel() 211 212 e := new(mockExporter) 213 e.SetDefaultExportApp() 214 215 sys := NewExportSystem(t, e.Export) 216 _ = sys.MustRun(t, "init", "some_moniker") 217 _ = sys.MustRun(t, "export") 218 219 require.True(t, e.WasCalled) 220 221 require.Equal(t, int64(-1), e.Called.Height) 222 require.False(t, e.Called.ForZeroHeight) 223 require.Empty(t, e.Called.JailAllowedAddrs) 224 require.Empty(t, e.Called.ModulesToExport) 225 }) 226 227 t.Run("passes flag values to the AppExporter", func(t *testing.T) { 228 t.Parallel() 229 230 e := new(mockExporter) 231 e.SetDefaultExportApp() 232 233 sys := NewExportSystem(t, e.Export) 234 _ = sys.MustRun(t, "init", "some_moniker") 235 _ = sys.MustRun(t, "export", 236 "--height=100", 237 "--jail-allowed-addrs", "addr1,addr2", 238 "--modules-to-export", "foo,bar", 239 ) 240 241 require.True(t, e.WasCalled) 242 243 require.Equal(t, int64(100), e.Called.Height) 244 require.False(t, e.Called.ForZeroHeight) 245 require.Equal(t, []string{"addr1", "addr2"}, e.Called.JailAllowedAddrs) 246 require.Equal(t, []string{"foo", "bar"}, e.Called.ModulesToExport) 247 }) 248 249 t.Run("passes --for-zero-height to the AppExporter", func(t *testing.T) { 250 t.Parallel() 251 252 e := new(mockExporter) 253 e.SetDefaultExportApp() 254 255 sys := NewExportSystem(t, e.Export) 256 _ = sys.MustRun(t, "init", "some_moniker") 257 _ = sys.MustRun(t, "export", "--for-zero-height") 258 259 require.True(t, e.WasCalled) 260 261 require.Equal(t, int64(-1), e.Called.Height) 262 require.True(t, e.Called.ForZeroHeight) 263 require.Empty(t, e.Called.JailAllowedAddrs) 264 require.Empty(t, e.Called.ModulesToExport) 265 }) 266 267 t.Run("prints to a given file with --output-document", func(t *testing.T) { 268 t.Parallel() 269 270 e := new(mockExporter) 271 e.SetDefaultExportApp() 272 273 sys := NewExportSystem(t, e.Export) 274 _ = sys.MustRun(t, "init", "some_moniker") 275 276 outDir := t.TempDir() 277 outFile := filepath.Join(outDir, "export.json") 278 279 res := sys.MustRun(t, "export", "--output-document", outFile) 280 281 require.Empty(t, res.Stderr.String()) 282 require.Empty(t, res.Stdout.String()) 283 284 j, err := os.ReadFile(outFile) 285 require.NoError(t, err) 286 287 CheckExportedGenesis(t, j) 288 }) 289 290 t.Run("prints genesis to stdout when no app exporter defined", func(t *testing.T) { 291 t.Parallel() 292 293 sys := NewExportSystem(t, nil) 294 _ = sys.MustRun(t, "init", "some_moniker") 295 296 res := sys.MustRun(t, "export") 297 298 require.Contains(t, res.Stderr.String(), "WARNING: App exporter not defined.") 299 300 origGenesis, err := os.ReadFile(filepath.Join(sys.HomeDir, "config", "genesis.json")) 301 require.NoError(t, err) 302 303 out := res.Stdout.Bytes() 304 305 require.Equal(t, origGenesis, out) 306 }) 307 308 t.Run("returns app exporter error", func(t *testing.T) { 309 t.Parallel() 310 311 e := new(mockExporter) 312 e.Err = fmt.Errorf("whoopsie") 313 314 sys := NewExportSystem(t, e.Export) 315 _ = sys.MustRun(t, "init", "some_moniker") 316 317 res := sys.Run("export") 318 319 require.ErrorIs(t, res.Err, e.Err) 320 }) 321 322 t.Run("rejects positional arguments", func(t *testing.T) { 323 t.Parallel() 324 325 e := new(mockExporter) 326 e.SetDefaultExportApp() 327 328 sys := NewExportSystem(t, e.Export) 329 _ = sys.MustRun(t, "init", "some_moniker") 330 331 outDir := t.TempDir() 332 outFile := filepath.Join(outDir, "export.json") 333 334 res := sys.Run("export", outFile) 335 require.Error(t, res.Err) 336 337 require.NoFileExists(t, outFile) 338 }) 339 } 340 341 // CheckExportedGenesis fails t if j cannot be unmarshaled into a valid AppGenesis. 342 func CheckExportedGenesis(t *testing.T, j []byte) { 343 t.Helper() 344 345 var ag genutiltypes.AppGenesis 346 require.NoError(t, json.Unmarshal(j, &ag)) 347 348 require.NotEmpty(t, ag.AppName) 349 require.NotZero(t, ag.GenesisTime) 350 require.NotEmpty(t, ag.ChainID) 351 require.NotNil(t, ag.Consensus) 352 }