Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

276 lines
8.3 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql/driver"
"errors"
"strings"
"sync"
"testing"
"time"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
)
// requireQueryTimeout asserts that err represents a query-timeout cancellation.
// Three error shapes are possible depending on timing in the pq driver:
// - context.DeadlineExceeded — context fires before the query reaches the server.
// - pq.Error{Code:"57014"} — PostgreSQL cancels the in-flight query and returns
// "canceling statement due to user request" on the original connection.
// - driver.ErrBadConn — pq's watchCancel goroutine sets cn.err on the client side
// before the query loop reads the server's 57014 response; pq short-circuits and
// returns this sentinel rather than the server error.
func requireQueryTimeout(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
var pqErr *pq.Error
switch {
case errors.As(err, &pqErr):
require.Equal(t, "57014", string(pqErr.Code))
case errors.Is(err, driver.ErrBadConn):
// client-side short-circuit; see comment above
default:
require.ErrorIs(t, err, context.DeadlineExceeded)
}
}
func TestSqlX(t *testing.T) {
t.Run("NamedParse", func(t *testing.T) {
queries := []struct {
in string
out string
}{
{
in: `SELECT pg_sleep(:Timeout)`,
out: `SELECT pg_sleep(:timeout)`,
},
{
in: `SELECT u.Username FROM Bots
LIMIT
:Limit
OFFSET
:Offset`,
out: `SELECT u.Username FROM Bots
LIMIT
:limit
OFFSET
:offset`,
},
{
in: `UPDATE OAuthAccessData SET Token =:Token`,
out: `UPDATE OAuthAccessData SET Token =:token`,
},
}
for _, q := range queries {
out := namedParamRegex.ReplaceAllStringFunc(q.in, strings.ToLower)
assert.Equal(t, q.out, out)
}
})
}
func makeStoreWithTimeout(t *testing.T, driver string, queryTimeout time.Duration) *SqlStore {
t.Helper()
settings, err := makeSqlSettings(driver)
if err != nil {
t.Skip(err)
}
*settings.QueryTimeout = int(queryTimeout.Seconds())
store := &SqlStore{
settings: settings,
logger: mlog.CreateConsoleTestLogger(t),
quitMonitor: make(chan struct{}),
wgMonitor: &sync.WaitGroup{},
}
require.NoError(t, store.initConnection())
t.Cleanup(store.Close)
return store
}
func TestSqlxQuery(t *testing.T) {
store := makeStoreWithTimeout(t, model.DatabaseDriverPostgres, 1*time.Second)
t.Run("db/happy", func(t *testing.T) {
rows, err := store.GetMaster().Query("SELECT 1")
require.NoError(t, err)
defer rows.Close()
require.True(t, rows.Next())
var v int
require.NoError(t, rows.Scan(&v))
require.Equal(t, 1, v)
})
t.Run("db/timeout", func(t *testing.T) {
_, err := store.GetMaster().Query("SELECT pg_sleep(2)")
requireQueryTimeout(t, err)
})
t.Run("tx/happy", func(t *testing.T) {
tx, err := store.GetMaster().Begin()
require.NoError(t, err)
defer func() { assert.NoError(t, tx.Rollback()) }()
rows, err := tx.Query("SELECT 1")
require.NoError(t, err)
defer rows.Close()
require.True(t, rows.Next())
var v int
require.NoError(t, rows.Scan(&v))
require.Equal(t, 1, v)
})
t.Run("tx/timeout", func(t *testing.T) {
tx, err := store.GetMaster().Begin()
require.NoError(t, err)
_, err = tx.Query("SELECT pg_sleep(2)")
require.Error(t, tx.Rollback()) // connection is dead after timeout
requireQueryTimeout(t, err)
})
}
func TestSqlxQueryRow(t *testing.T) {
store := makeStoreWithTimeout(t, model.DatabaseDriverPostgres, 1*time.Second)
t.Run("db/happy", func(t *testing.T) {
var v int
require.NoError(t, store.GetMaster().QueryRow("SELECT 1").Scan(&v))
require.Equal(t, 1, v)
})
t.Run("db/timeout", func(t *testing.T) {
var v int
err := store.GetMaster().QueryRow("SELECT 1 FROM (SELECT pg_sleep(2)) s").Scan(&v)
requireQueryTimeout(t, err)
})
t.Run("tx/happy", func(t *testing.T) {
tx, err := store.GetMaster().Begin()
require.NoError(t, err)
defer func() { assert.NoError(t, tx.Rollback()) }()
var v int
require.NoError(t, tx.QueryRow("SELECT 1").Scan(&v))
require.Equal(t, 1, v)
})
t.Run("tx/timeout", func(t *testing.T) {
tx, err := store.GetMaster().Begin()
require.NoError(t, err)
var v int
err = tx.QueryRow("SELECT 1 FROM (SELECT pg_sleep(2)) s").Scan(&v)
require.Error(t, tx.Rollback()) // connection is dead after timeout
requireQueryTimeout(t, err)
})
}
func TestEnsureQueryTimeout(t *testing.T) {
t.Run("no deadline adds timeout", func(t *testing.T) {
timeout := 30 * time.Second
ctx, cancel := ensureQueryTimeout(context.Background(), timeout)
defer cancel()
deadline, ok := ctx.Deadline()
require.True(t, ok)
require.WithinDuration(t, time.Now().Add(timeout), deadline, time.Second)
})
t.Run("existing deadline is preserved", func(t *testing.T) {
originalDeadline := time.Now().Add(5 * time.Minute)
parent, parentCancel := context.WithDeadline(context.Background(), originalDeadline)
defer parentCancel()
ctx, cancel := ensureQueryTimeout(parent, 30*time.Second)
defer cancel()
deadline, ok := ctx.Deadline()
require.True(t, ok)
require.Equal(t, originalDeadline, deadline)
})
t.Run("no-op cancel is safe to call", func(t *testing.T) {
parent, parentCancel := context.WithTimeout(context.Background(), time.Minute)
defer parentCancel()
_, cancel := ensureQueryTimeout(parent, 30*time.Second)
require.NotPanics(t, func() { cancel() }) // calling the no-op cancel must not panic
})
t.Run("noTimeoutKey suppresses timeout injection", func(t *testing.T) {
ctx := context.WithValue(context.Background(), noTimeoutKey{}, true)
newCtx, cancel := ensureQueryTimeout(ctx, 30*time.Second)
defer cancel()
_, ok := newCtx.Deadline()
require.False(t, ok)
})
}
func TestSqlxSelectContext(t *testing.T) {
store := makeStoreWithTimeout(t, model.DatabaseDriverPostgres, 1*time.Second)
t.Run("adds timeout when context has none", func(t *testing.T) {
// context.Background() has no deadline — wrapper adds 1s; pg_sleep(2) times out.
var result []string
err := store.GetMaster().SelectContext(context.Background(), &result, "SELECT pg_sleep(2)")
requireQueryTimeout(t, err)
})
t.Run("respects caller deadline shorter than wrapper timeout", func(t *testing.T) {
// 1ns context fires before the query reaches the server.
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
var result []string
err := store.GetMaster().SelectContext(ctx, &result, "SELECT pg_sleep(2)")
requireQueryTimeout(t, err)
})
t.Run("respects caller deadline longer than wrapper timeout", func(t *testing.T) {
// Caller supplies a 5s deadline; wrapper queryTimeout is 1s.
// With "add only if missing" the 5s deadline is used — the 2s sleep completes.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var result []int
err := store.GetMaster().SelectContext(ctx, &result, "SELECT 1 FROM (SELECT pg_sleep(2)) s")
require.NoError(t, err)
require.Equal(t, []int{1}, result)
})
}
func TestSqlxSelect(t *testing.T) {
store := makeStoreWithTimeout(t, model.DatabaseDriverPostgres, 1*time.Second)
t.Run("SelectCtx", func(t *testing.T) {
var result []string
err := store.GetMaster().SelectContext(context.Background(), &result, "SELECT 'test' AS col")
require.NoError(t, err)
require.Equal(t, []string{"test"}, result)
// Test timeout
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
query := "SELECT pg_sleep(2)"
err = store.GetMaster().SelectContext(ctx, &result, query)
requireQueryTimeout(t, err)
})
t.Run("SelectBuilderCtx", func(t *testing.T) {
var result []string
builder := store.getQueryBuilder().
Select("'test' AS col")
err := store.GetMaster().SelectBuilderCtx(context.Background(), &result, builder)
require.NoError(t, err)
require.Equal(t, []string{"test"}, result)
// Test timeout
ctx, cancel := context.WithTimeout(context.Background(), 1)
defer cancel()
builder = store.getQueryBuilder().
Select("pg_sleep(2)")
err = store.GetMaster().SelectBuilderCtx(ctx, &result, builder)
requireQueryTimeout(t, err)
})
}