Skip to content

Commit 38c8d97

Browse files
committed
fix(cmd): prevent test DB connection leak via app.Before config swap
TestShell_BeforeNode and TestShell_RunNode_WithBeforeNode were calling app.Before(c) after pre-setting shell.Config with a txdb-wrapped config via configtest.NewGeneralConfig. app.Before unconditionally ran opts.New() → CoreDefaults() which set Database.DriverName back to pgx and assigned the fresh config to s.Config, silently overwriting the test's txdb config. BeforeNode → pg.NewLockedDB then opened real pgx connections against the shared chainlink_test database, persisting keystore state and leaking it to other tests (flaking Test_CSAKeyStore_E2E) and eating slots from the server-wide max_connections budget (causing the mass 25-minute chan-receive timeouts in CORE-2388). Three changes: 1. core/cmd/app.go — guard the config creation in app.Before so a pre-set s.Config is preserved. Defense-in-depth: any future test that sets its own config and goes through app.Before is protected from the same silent swap. 2. core/cmd/shell_local.go — nil-guard CloseLogger in afterNode. Tests that call BeforeNode directly (without app.Before) never set it. app.After already has this nil check; afterNode was missing it. 3. core/cmd/shell_local_test.go — remove the unnecessary app.Before(c) calls from the two affected tests. BeforeNode only needs Config and Logger, both already set directly; other tests in the same file already build the shell this way. Also drop the incorrect_password subtests — they were load-bearing on the leak (the correct_password subtest would persist a CSA key to the shared DB, which the incorrect_password subtest would then fail to decrypt, producing the expected error). The wrong-password-against-populated-keyring invariant is already covered at the correct layer by TestMasterKeystore_Unlock_Save/won't_load_a_saved_keyRing_if_the_password_is_incorrect in core/services/keystore. Testing it through the CLI would require cross-connection state sharing that chainlink's txdb path does not support: chainlink-common/pkg/sqlutil/pg/pg.go:69 generates a fresh UUID DSN per pg.NewConnection call, isolating every pool into its own transaction. Refs: CORE-2388, CORE-2370 Supersedes: #21504
1 parent d62edd5 commit 38c8d97

3 files changed

Lines changed: 43 additions & 42 deletions

File tree

core/cmd/app.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,21 @@ func NewApp(s *Shell) *cli.App {
8080
// logger instead.
8181
lggr, closeFn := logger.NewLogger()
8282

83-
cfg, err := opts.New()
84-
if err != nil {
85-
return err
86-
}
87-
8883
s.Logger = lggr
8984
s.Registerer = prometheus.DefaultRegisterer // use the global DefaultRegisterer, should be safe since we only ever run one instance of the app per shell
9085
s.CloseLogger = closeFn
91-
s.Config = cfg
86+
87+
// Only create a new config from opts if one hasn't been pre-set.
88+
// Tests pre-set s.Config with a txdb-wrapped driver for DB isolation;
89+
// unconditionally overwriting it here causes real pgx connections to
90+
// leak between tests and exhaust the connection pool.
91+
if s.Config == nil {
92+
cfg, err := opts.New()
93+
if err != nil {
94+
return err
95+
}
96+
s.Config = cfg
97+
}
9298

9399
if c.Bool("json") {
94100
s.Renderer = RendererJSON{Writer: os.Stdout}
@@ -122,11 +128,11 @@ func NewApp(s *Shell) *cli.App {
122128

123129
// Allow for initServerConfig to be called if the flag is provided.
124130
if c.Bool("applyInitServerConfig") {
125-
cfg, err = initServerConfig(&opts, s.configFiles, s.secretsFiles)
131+
serverCfg, err := initServerConfig(&opts, s.configFiles, s.secretsFiles)
126132
if err != nil {
127133
return err
128134
}
129-
s.Config = cfg
135+
s.Config = serverCfg
130136
}
131137

132138
return nil

core/cmd/shell_local.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,8 +1241,10 @@ func (s *Shell) afterNode(lggr logger.SugaredLogger) {
12411241
}
12421242
lggr.Debug("Closed DB")
12431243

1244-
if err := s.CloseLogger(); err != nil {
1245-
log.Printf("Failed to close Logger: %v", err)
1244+
if s.CloseLogger != nil {
1245+
if err := s.CloseLogger(); err != nil {
1246+
log.Printf("Failed to close Logger: %v", err)
1247+
}
12461248
}
12471249
})
12481250
}

core/cmd/shell_local_test.go

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,18 @@ func TestShell_RemoveBlocks(t *testing.T) {
507507

508508
func TestShell_BeforeNode(t *testing.T) {
509509
testutils.SkipShortDB(t)
510+
// Note: the "wrong password against an existing key ring" case is
511+
// intentionally covered by TestMasterKeystore_Unlock_Save in
512+
// core/services/keystore, not here. That is a keystore invariant, and
513+
// asserting it through the CLI would require cross-connection state
514+
// sharing that chainlink's txdb path does not support (pg.NewConnection
515+
// generates a fresh UUID DSN per call — chainlink-common/pkg/sqlutil/pg/pg.go:69).
510516
tests := []struct {
511517
name string
512518
pwdfile string
513519
wantUnlocked bool
514520
}{
515521
{"correct password", "../internal/fixtures/correct_password.txt", true},
516-
{"incorrect password", "../internal/fixtures/incorrect_password.txt", false},
517522
{"wrong file", "doesntexist.txt", false},
518523
}
519524

@@ -543,15 +548,11 @@ func TestShell_BeforeNode(t *testing.T) {
543548

544549
c := cli.NewContext(nil, set, nil)
545550

546-
// Create full CLI app and run the Before hook first
547-
app := cmd.NewApp(&shell)
548-
err := app.Before(c)
549-
if err != nil && test.wantUnlocked {
550-
t.Fatalf("CLI Before hook failed: %v", err)
551-
}
552-
553-
// Run before hook to initialize components with authentication
554-
err = shell.BeforeNode(c)
551+
// BeforeNode only reads Config and Logger, both already set
552+
// above. Going through app.Before would silently overwrite
553+
// s.Config with a fresh pgx-driven one from CoreDefaults and
554+
// break txdb isolation — see CORE-2388 and the guard in app.go.
555+
err := shell.BeforeNode(c)
555556

556557
if test.wantUnlocked {
557558
require.NoError(t, err)
@@ -577,13 +578,14 @@ func TestShell_BeforeNode(t *testing.T) {
577578
}
578579

579580
func TestShell_RunNode_WithBeforeNode(t *testing.T) {
581+
// Note: the wrong-password case is covered by TestMasterKeystore_Unlock_Save
582+
// in core/services/keystore, not here. See TestShell_BeforeNode for details.
580583
tests := []struct {
581584
name string
582585
pwdfile string
583586
expectStart bool
584587
}{
585588
{"correct password", "../internal/fixtures/correct_password.txt", true},
586-
{"incorrect password", "../internal/fixtures/incorrect_password.txt", false},
587589
}
588590

589591
for _, test := range tests {
@@ -641,29 +643,20 @@ func TestShell_RunNode_WithBeforeNode(t *testing.T) {
641643

642644
c := cli.NewContext(nil, set, nil)
643645

644-
// First initialize components (this includes authentication)
645-
cliApp := cmd.NewApp(&shell)
646-
err := cliApp.Before(c)
647-
require.NoError(t, err)
648-
649-
err = shell.BeforeNode(c)
646+
// BeforeNode only needs Config and Logger, both already set.
647+
// Avoid app.Before here — see CORE-2388 / TestShell_BeforeNode.
648+
err := shell.BeforeNode(c)
649+
require.NoError(t, err, "BeforeNode should succeed")
650+
assert.NotNil(t, shell.KeyStore)
651+
assert.NotNil(t, shell.DS)
652+
assert.NotNil(t, shell.LDB)
650653

651-
if test.expectStart {
652-
require.NoError(t, err, "BeforeNode should succeed")
653-
// Verify components are initialized
654-
assert.NotNil(t, shell.KeyStore)
655-
assert.NotNil(t, shell.DS)
656-
assert.NotNil(t, shell.LDB)
654+
// Now test RunNode with pre-authenticated keystore
655+
// Note: RunNode will start the app but we expect it to work since keystore is authenticated
656+
err = shell.RunNode(c)
657+
require.NoError(t, err, "RunNode should succeed with authenticated keystore")
658+
assert.Equal(t, 1, apiPrompt.Count, "API should be initialized")
657659

658-
// Now test RunNode with pre-authenticated keystore
659-
// Note: RunNode will start the app but we expect it to work since keystore is authenticated
660-
err = shell.RunNode(c)
661-
require.NoError(t, err, "RunNode should succeed with authenticated keystore")
662-
assert.Equal(t, 1, apiPrompt.Count, "API should be initialized")
663-
} else {
664-
require.Error(t, err, "BeforeNode should fail with incorrect password")
665-
// Don't test RunNode if BeforeNode failed
666-
}
667660
// Clean up database if it was opened
668661
if shell.LDB != nil {
669662
cleanupErr := shell.AfterNode(c)

0 commit comments

Comments
 (0)