//go:build test package db import ( "context" "errors" "testing" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================================= // FailingTx — wraps a real pgx.Tx and fails on configured operations // ============================================================================= // FailingTx wraps a pgx.Tx and injects failures on configured operations. // All unmodified methods delegate to the real transaction via embedding. type FailingTx struct { pgx.Tx // delegate all methods to real tx failBegin bool // return self instead of savepoint failExec bool failCommit bool execErr error commitErr error } // Begin returns self instead of creating a savepoint, so subsequent // Exec/Commit calls go through our wrapper's failure checks. func (f *FailingTx) Begin(ctx context.Context) (pgx.Tx, error) { if f.failBegin { return nil, errors.New("simulated begin failure") } return f, nil } // Exec fails with the configured error if failExec is true. func (f *FailingTx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { if f.failExec { return pgconn.CommandTag{}, f.execErr } return f.Tx.Exec(ctx, sql, args...) } // Commit fails with the configured error if failCommit is true. func (f *FailingTx) Commit(ctx context.Context) error { if f.failCommit { return f.commitErr } return f.Tx.Commit(ctx) } // ============================================================================= // FailingPoolProxy — wraps *PoolProxy and returns FailingTx from Begin // ============================================================================= // FailingPoolProxy wraps a *PoolProxy and overrides Begin to return a // FailingTx that can fail on Exec or Commit. All other methods (Exec, // Query, QueryRow, Ping, Acquire) delegate to the embedded PoolProxy. type FailingPoolProxy struct { *PoolProxy failExec bool failCommit bool execErr error commitErr error } // Begin starts a transaction wrapped in a FailingTx that respects the // configured failure modes. If the underlying Begin fails, the error // propagates as-is. func (f *FailingPoolProxy) Begin(ctx context.Context) (pgx.Tx, error) { realTx, err := f.PoolProxy.Begin(ctx) if err != nil { return nil, err } return &FailingTx{ Tx: realTx, failExec: f.failExec, failCommit: f.failCommit, execErr: f.execErr, commitErr: f.commitErr, }, nil } // ============================================================================= // WithFailingConn — convenience constructor for FailingPoolProxy // ============================================================================= // WithFailingConn creates a FailingPoolProxy with default error messages // and the given failure modes. func WithFailingConn(original *PoolProxy, failExec, failCommit bool) *FailingPoolProxy { return &FailingPoolProxy{ PoolProxy: original, failExec: failExec, failCommit: failCommit, execErr: errors.New("simulated exec failure"), commitErr: errors.New("simulated commit failure"), } } // ============================================================================= // Tests // ============================================================================= func TestFailingTx_ExecFailure(t *testing.T) { closePool() resetEnv() err := Connect() require.NoError(t, err) defer closePool() ctx := context.Background() pool := Conn.Pool() realTx, err := pool.Begin(ctx) require.NoError(t, err) defer realTx.Rollback(ctx) //nolint:errcheck ftx := &FailingTx{Tx: realTx, failExec: true, execErr: errors.New("disk full")} // Begin returns self tx2, err := ftx.Begin(ctx) require.NoError(t, err) _, ok := tx2.(*FailingTx) assert.True(t, ok, "should return FailingTx") // Exec fails _, err = ftx.Exec(ctx, "SELECT 1") assert.ErrorContains(t, err, "disk full") // Commit still works (not set to fail) err = ftx.Commit(ctx) assert.NoError(t, err) } func TestFailingTx_CommitFailure(t *testing.T) { closePool() resetEnv() err := Connect() require.NoError(t, err) defer closePool() ctx := context.Background() pool := Conn.Pool() realTx, err := pool.Begin(ctx) require.NoError(t, err) defer realTx.Rollback(ctx) //nolint:errcheck ftx := &FailingTx{Tx: realTx, failCommit: true, commitErr: errors.New("commit failed")} // Exec works (not set to fail) _, err = ftx.Exec(ctx, "SELECT 1") assert.NoError(t, err) // Commit fails err = ftx.Commit(ctx) assert.ErrorContains(t, err, "commit failed") } func TestFailingPoolProxy_BeginReturnsFailingTx(t *testing.T) { closePool() resetEnv() err := Connect() require.NoError(t, err) defer closePool() ctx := context.Background() fp := &FailingPoolProxy{ PoolProxy: Conn, failCommit: true, commitErr: errors.New("commit failed"), } tx, err := fp.Begin(ctx) require.NoError(t, err) defer tx.Rollback(ctx) //nolint:errcheck _, ok := tx.(*FailingTx) assert.True(t, ok, "should return FailingTx") // Exec works (not set to fail) _, err = tx.Exec(ctx, "SELECT 1") assert.NoError(t, err) // Commit fails err = tx.Commit(ctx) assert.ErrorContains(t, err, "commit failed") }