package test import ( "RenewCMS/internal/domain/transaction" "RenewCMS/internal/infrastructure/persistence/database" persistence "RenewCMS/internal/infrastructure/persistence/repositories" "context" "errors" "fmt" "testing" mysqldriver "github.com/go-sql-driver/mysql" "github.com/jackc/pgx/v5/pgconn" ) type codedSQLiteError struct{ code int } func (c codedSQLiteError) Error() string { return fmt.Sprintf("sqlite code %d", c.code) } func (c codedSQLiteError) Code() int { return c.code } func TestDriverLockContention(t *testing.T) { cases := []struct { name string driver string err error want bool }{ {"sqlite busy", database.DriverSQLite, codedSQLiteError{5}, true}, {"sqlite busy snapshot", database.DriverSQLite, fmt.Errorf("wrapped: %w", codedSQLiteError{5 | 2<<8}), true}, {"sqlite locked", database.DriverSQLite, codedSQLiteError{6}, true}, {"sqlite constraint", database.DriverSQLite, codedSQLiteError{19}, false}, {"sqlite plain error", database.DriverSQLite, errors.New("database is locked"), false}, {"mysql nowait", database.DriverMySQL, &mysqldriver.MySQLError{Number: 3572}, true}, {"mysql lock wait timeout", database.DriverMySQL, fmt.Errorf("wrapped: %w", &mysqldriver.MySQLError{Number: 1205}), true}, {"mysql deadlock", database.DriverMySQL, fmt.Errorf("wrapped: %w", &mysqldriver.MySQLError{Number: 1213}), true}, {"mariadb record changed since last read", database.DriverMySQL, &mysqldriver.MySQLError{Number: 1020}, true}, {"mysql duplicate", database.DriverMySQL, &mysqldriver.MySQLError{Number: 1062}, false}, {"postgres lock not available", database.DriverPostgres, &pgconn.PgError{Code: "55P03"}, true}, {"postgres deadlock detected", database.DriverPostgres, &pgconn.PgError{Code: "40P01"}, true}, {"postgres serialization failure", database.DriverPostgres, fmt.Errorf("wrapped: %w", &pgconn.PgError{Code: "40001"}), true}, {"postgres unique violation", database.DriverPostgres, &pgconn.PgError{Code: "23505"}, false}, {"unknown driver", "oracle", codedSQLiteError{5}, false}, {"nil error", database.DriverSQLite, nil, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := database.IsLockContentionFor(c.driver, c.err); got != c.want { t.Errorf("got %v, want %v", got, c.want) } }) } } func TestTransactorClassifiesLockContention(t *testing.T) { db := openContractSQLite(t) transactor := persistence.NewTransactor(db) run := func(failure error) error { return transactor.Run(context.Background(), func(transaction.Repositories) error { return failure }) } t.Run("a busy error becomes the domain sentinel and keeps its cause", func(t *testing.T) { cause := codedSQLiteError{5} err := run(cause) if !errors.Is(err, transaction.ErrLockContention) || !errors.Is(err, cause) { t.Errorf("err = %v", err) } }) t.Run("another error is returned untouched", func(t *testing.T) { other := errors.New("other") if err := run(other); err != other { t.Errorf("err = %v", err) } }) t.Run("success stays nil", func(t *testing.T) { if err := run(nil); err != nil { t.Errorf("err = %v", err) } }) }