snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
322
server/cmd/mattermost/commands/db_ping_test.go
Normal file
322
server/cmd/mattermost/commands/db_ping_test.go
Normal file
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
dbsql "database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
// dsnFromHelper builds a postgres:// DSN from the test main helper's SqlSettings.
|
||||
// The helper itself stores the live test postgres DSN.
|
||||
func dsnFromHelper(t *testing.T) string {
|
||||
t.Helper()
|
||||
require.NotNil(t, mainHelper, "mainHelper must be initialized; do not run with -short")
|
||||
settings := mainHelper.GetSQLSettings()
|
||||
require.NotNil(t, settings.DataSource)
|
||||
require.NotEmpty(t, *settings.DataSource)
|
||||
return *settings.DataSource
|
||||
}
|
||||
|
||||
// --- subprocess (CLI integration) tests ---
|
||||
|
||||
func TestDBPingHappyPath(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires live test database")
|
||||
}
|
||||
|
||||
th := SetupWithStoreMock(t)
|
||||
output := th.CheckCommand(t, "db", "ping", "--timeout", "30s")
|
||||
require.Contains(t, output, "Database is reachable",
|
||||
"expected success log line in command output, got: %s", output)
|
||||
}
|
||||
|
||||
func TestDBPingDirectDSN(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires live test database")
|
||||
}
|
||||
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
dsn := dsnFromHelper(t)
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping", "--timeout", "30s")
|
||||
require.NoError(t, err, "command should succeed when DSN is direct postgres URL; output: %s", output)
|
||||
require.Contains(t, output, "Database is reachable")
|
||||
}
|
||||
|
||||
func TestDBPingTimeoutOnUnreachableDB(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
// Loopback to a port nothing listens on; connect_timeout=1 keeps each
|
||||
// attempt short. We allow 2s total with 500ms between attempts so we get
|
||||
// multiple "Waiting for database" lines.
|
||||
dsn := "postgres://nobody@127.0.0.1:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
|
||||
start := time.Now()
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "2s", "--retry-interval", "500ms")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err, "command should fail on unreachable DB; output: %s", output)
|
||||
require.Contains(t, output, "timed out waiting for database",
|
||||
"expected timeout message in output, got: %s", output)
|
||||
require.LessOrEqual(t, elapsed, 30*time.Second,
|
||||
"command should not exceed a generous upper bound; took %s", elapsed)
|
||||
}
|
||||
|
||||
func TestDBPingInvalidDSN(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
// Passes IsDatabaseDSN (postgres:// prefix) so it takes the direct path.
|
||||
dsn := "postgres://leakyuser:supersecret@[invalid"
|
||||
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "2s", "--retry-interval", "500ms")
|
||||
require.Error(t, err, "command should fail on malformed DSN; output: %s", output)
|
||||
require.Contains(t, output, "invalid database DSN",
|
||||
"expected sanitized DSN parse error; got: %s", output)
|
||||
require.Contains(t, output, "missing ']' in host",
|
||||
"expected malformed DSN reason; got: %s", output)
|
||||
require.NotContains(t, output, "supersecret",
|
||||
"malformed DSN errors must not leak credentials; got: %s", output)
|
||||
require.NotContains(t, output, "leakyuser",
|
||||
"malformed DSN errors must not leak credentials; got: %s", output)
|
||||
}
|
||||
|
||||
func TestDBPingMissingConfigFile(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
// Point --config at a path that does not exist; createFileIfNotExist=false
|
||||
// inside resolvePingDataSource means NewStoreFromDSN will return an error.
|
||||
missing := th.TemporaryDirectory() + "/does-not-exist.json"
|
||||
|
||||
output, err := th.RunCommandWithOutput(t, "--config", missing, "db", "ping",
|
||||
"--timeout", "2s", "--retry-interval", "500ms")
|
||||
require.Error(t, err, "command should fail when --config file does not exist; output: %s", output)
|
||||
require.Contains(t, output, "failed to load configuration",
|
||||
"expected config-load error message; got: %s", output)
|
||||
}
|
||||
|
||||
func TestDBPingFlagValidation(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
dsn := "postgres://localhost:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
|
||||
t.Run("zero timeout", func(t *testing.T) {
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "0s")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, output, "--timeout must be > 0",
|
||||
"expected timeout validation error; got: %s", output)
|
||||
})
|
||||
|
||||
t.Run("zero retry interval", func(t *testing.T) {
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "1s", "--retry-interval", "0s")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, output, "--retry-interval must be > 0",
|
||||
"expected retry-interval validation error; got: %s", output)
|
||||
})
|
||||
|
||||
t.Run("negative timeout", func(t *testing.T) {
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "-1s")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, output, "--timeout must be > 0",
|
||||
"expected timeout validation error for negative value; got: %s", output)
|
||||
})
|
||||
|
||||
t.Run("garbage timeout value", func(t *testing.T) {
|
||||
// cobra refuses to parse "garbage" as a duration; subcommand never runs.
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "garbage")
|
||||
require.Error(t, err)
|
||||
// Don't pin to exact text — cobra owns the error string here. Just
|
||||
// confirm the subcommand's success log is absent.
|
||||
require.NotContains(t, output, "Database is reachable",
|
||||
"command should not have run successfully; got: %s", output)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDBPingRetryIntervalHonored(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
dsn := "postgres://nobody@127.0.0.1:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
|
||||
start := time.Now()
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "2s", "--retry-interval", "500ms")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
// Loose lower bound: at 500ms intervals we expect at least 2 retries
|
||||
// (~1s wall clock minimum) before the 2s timeout strikes.
|
||||
require.GreaterOrEqual(t, elapsed, 1*time.Second,
|
||||
"expected retries to span at least 1s; got %s", elapsed)
|
||||
// Loose upper bound: don't exceed several multiples of the configured
|
||||
// timeout — accommodates CI variance.
|
||||
require.LessOrEqual(t, elapsed, 30*time.Second,
|
||||
"expected command to bail close to --timeout; took %s", elapsed)
|
||||
|
||||
waitingCount := strings.Count(output, "Waiting for database")
|
||||
require.GreaterOrEqual(t, waitingCount, 2,
|
||||
"expected at least 2 'Waiting for database' lines; got %d in output:\n%s",
|
||||
waitingCount, output)
|
||||
}
|
||||
|
||||
// TestDBPingShortRetryIntervalProducesMoreAttempts verifies that the
|
||||
// --retry-interval flag actually controls the cadence (not just the timeout).
|
||||
func TestDBPingShortRetryIntervalProducesMoreAttempts(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
th.SetAutoConfig(false)
|
||||
|
||||
dsn := "postgres://nobody@127.0.0.1:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
|
||||
output, err := th.RunCommandWithOutput(t, "--config", dsn, "db", "ping",
|
||||
"--timeout", "3s", "--retry-interval", "200ms")
|
||||
require.Error(t, err)
|
||||
|
||||
waitingCount := strings.Count(output, "Waiting for database")
|
||||
// At 200ms intervals over 3s we expect well more than 3 attempts even
|
||||
// accounting for per-attempt connection overhead.
|
||||
require.GreaterOrEqual(t, waitingCount, 3,
|
||||
"expected several retries with short interval; got %d in output:\n%s",
|
||||
waitingCount, output)
|
||||
}
|
||||
|
||||
// TestDBPingCmdRegistered confirms the new subcommand is wired into the
|
||||
// existing DbCmd group, so users actually get `mattermost db ping`.
|
||||
func TestDBPingCmdRegistered(t *testing.T) {
|
||||
require.Contains(t, DbCmd.Commands(), DBPingCmd,
|
||||
"DBPingCmd should be registered as a subcommand of DbCmd")
|
||||
require.Equal(t, "ping", DBPingCmd.Use)
|
||||
|
||||
// Flags exist with sensible defaults.
|
||||
timeoutFlag := DBPingCmd.Flags().Lookup("timeout")
|
||||
require.NotNil(t, timeoutFlag)
|
||||
require.Equal(t, dbPingDefaultTimeout.String(), timeoutFlag.DefValue)
|
||||
|
||||
intervalFlag := DBPingCmd.Flags().Lookup("retry-interval")
|
||||
require.NotNil(t, intervalFlag)
|
||||
require.Equal(t, dbPingDefaultRetryInterval.String(), intervalFlag.DefValue)
|
||||
}
|
||||
|
||||
// --- in-process tests of pingWithRetry / resolvePingDataSource ---
|
||||
|
||||
func TestPingWithRetry_SuccessOnFirstAttempt(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("requires live test database")
|
||||
}
|
||||
|
||||
dsn := dsnFromHelper(t)
|
||||
db, err := dbsql.Open(model.DatabaseDriverPostgres, dsn)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
err = pingWithRetry(ctx, db, 100*time.Millisecond, logger)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPingWithRetry_TimeoutAgainstUnreachable(t *testing.T) {
|
||||
dsn := "postgres://nobody@127.0.0.1:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
db, err := dbsql.Open(model.DatabaseDriverPostgres, dsn)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
err = pingWithRetry(ctx, db, 200*time.Millisecond, logger)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
require.True(t,
|
||||
errors.Is(err, context.DeadlineExceeded) ||
|
||||
strings.Contains(err.Error(), "timed out waiting for database"),
|
||||
"expected deadline-exceeded or timeout error; got %v", err)
|
||||
// Must have honored the timeout, not just returned immediately.
|
||||
require.LessOrEqual(t, elapsed, 30*time.Second,
|
||||
"expected reasonable upper bound; took %s", elapsed)
|
||||
}
|
||||
|
||||
func TestPingWithRetry_ContextCancelImmediately(t *testing.T) {
|
||||
dsn := "postgres://nobody@127.0.0.1:1/mattermost?sslmode=disable&connect_timeout=1"
|
||||
db, err := dbsql.Open(model.DatabaseDriverPostgres, dsn)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel before we even start
|
||||
|
||||
logger := mlog.CreateConsoleTestLogger(t)
|
||||
err = pingWithRetry(ctx, db, 1*time.Second, logger)
|
||||
require.Error(t, err, "cancelled context should produce an error")
|
||||
}
|
||||
|
||||
// In-process tests of resolvePingDataSource. We drive DSN selection via the
|
||||
// MM_CONFIG environment variable rather than the --config persistent flag
|
||||
// because the persistent flag is only merged into a subcommand's local
|
||||
// flagset during cobra's Execute() pipeline; calling resolvePingDataSource
|
||||
// directly outside Execute means the flag would not be visible.
|
||||
// MM_CONFIG is consumed by getConfigDSN as the second-precedence source.
|
||||
|
||||
func TestResolvePingDataSource_DirectDSN(t *testing.T) {
|
||||
wanted := "postgres://user:pw@example.invalid:5432/mm?sslmode=disable"
|
||||
t.Setenv("MM_CONFIG", wanted)
|
||||
|
||||
got, err := resolvePingDataSource(DBPingCmd)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wanted, got)
|
||||
}
|
||||
|
||||
func TestResolvePingDataSource_DirectDSN_PostgresqlScheme(t *testing.T) {
|
||||
wanted := "postgresql://user:pw@example.invalid:5432/mm?sslmode=disable"
|
||||
t.Setenv("MM_CONFIG", wanted)
|
||||
|
||||
got, err := resolvePingDataSource(DBPingCmd)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wanted, got)
|
||||
}
|
||||
|
||||
func TestResolvePingDataSource_MissingFile(t *testing.T) {
|
||||
missing := t.TempDir() + "/no-such-config.json"
|
||||
t.Setenv("MM_CONFIG", missing)
|
||||
|
||||
_, err := resolvePingDataSource(DBPingCmd)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to load configuration")
|
||||
}
|
||||
|
||||
// TestResolvePingDataSource_PointsAtDirectory verifies that pointing --config
|
||||
// at a directory (not a JSON file) surfaces a clear, wrapped error. Catches
|
||||
// regressions where we silently fall through instead of returning the load error.
|
||||
func TestResolvePingDataSource_PointsAtDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("MM_CONFIG", dir)
|
||||
|
||||
_, err := resolvePingDataSource(DBPingCmd)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to load configuration",
|
||||
"expected wrapped error; got %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user