diff --git a/database-no-sqlite.go b/database-no-sqlite.go index 0add7ff..db5c072 100644 --- a/database-no-sqlite.go +++ b/database-no-sqlite.go @@ -1,64 +1,65 @@ //go:build !sqlite && !wflib // +build !sqlite,!wflib /* * Copyright © 2019-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( + "errors" "github.com/go-sql-driver/mysql" "github.com/lib/pq" "github.com/writeas/web-core/log" ) func (db *datastore) isDuplicateKeyErr(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrDuplicateKey } } else if db.driverName == driverPostgres { - if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr, ok := errors.AsType[*pq.Error](err); ok { return postgresErr.Code == postgresErrDuplicateKey } } else { log.Error("isDuplicateKeyErr: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isIgnorableError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrCollationMix } } else { log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isHighLoadError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns } } if db.driverName == driverPostgres { - if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr, ok := errors.AsType[*pq.Error](err); ok { return postgresErr.Code == postgresErrMaxUserConns || postgresErr.Code == postgresErrTooManyConns } } return false } diff --git a/database-sqlite.go b/database-sqlite.go index e2372ea..fcaa1c8 100644 --- a/database-sqlite.go +++ b/database-sqlite.go @@ -1,85 +1,86 @@ //go:build sqlite && !wflib // +build sqlite,!wflib /* * Copyright © 2019-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" + "errors" "regexp" "github.com/go-sql-driver/mysql" "github.com/lib/pq" "github.com/mattn/go-sqlite3" "github.com/writeas/web-core/log" ) func init() { SQLiteEnabled = true regex := func(re, s string) (bool, error) { return regexp.MatchString(re, s) } sql.Register("sqlite3_with_regex", &sqlite3.SQLiteDriver{ ConnectHook: func(conn *sqlite3.SQLiteConn) error { return conn.RegisterFunc("regexp", regex, true) }, }) } func (db *datastore) isDuplicateKeyErr(err error) bool { if db.driverName == driverSQLite { if err, ok := err.(sqlite3.Error); ok { return err.Code == sqlite3.ErrConstraint } } else if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrDuplicateKey } } else if db.driverName == driverPostgres { - if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr, ok := errors.AsType[*pq.Error](err); ok { return postgresErr.Code == postgresErrDuplicateKey } } else { log.Error("isDuplicateKeyErr: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isIgnorableError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrCollationMix } } else { log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isHighLoadError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns } } if db.driverName == driverPostgres { - if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr, ok := errors.AsType[*pq.Error](err); ok { return postgresErr.Code == postgresErrMaxUserConns || postgresErr.Code == postgresErrTooManyConns } } return false } diff --git a/database_test.go b/database_test.go index 9cd9771..a28cb2d 100644 --- a/database_test.go +++ b/database_test.go @@ -1,79 +1,98 @@ package writefreely import ( "context" "database/sql" + "fmt" "testing" + "github.com/lib/pq" "github.com/stretchr/testify/assert" ) func TestOAuthDatastore(t *testing.T) { if !runMySQLTests() { t.Skip("skipping mysql tests") } withTestDB(t, func(db *sql.DB) { ctx := context.Background() ds := &datastore{ DB: db, driverName: "", } state, err := ds.GenerateOAuthState(ctx, "test", "development", 0, "") assert.NoError(t, err) assert.Len(t, state, 24) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = false", state) _, _, _, _, err = ds.ValidateOAuthState(ctx, state) assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = true", state) var localUserID int64 = 99 var remoteUserID = "100" err = ds.RecordRemoteUserID(ctx, localUserID, remoteUserID, "test", "test", "access_token_a") assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users` WHERE `user_id` = ? AND `remote_user_id` = ? AND access_token = 'access_token_a'", localUserID, remoteUserID) err = ds.RecordRemoteUserID(ctx, localUserID, remoteUserID, "test", "test", "access_token_b") assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users` WHERE `user_id` = ? AND `remote_user_id` = ? AND access_token = 'access_token_b'", localUserID, remoteUserID) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users`") foundUserID, err := ds.GetIDForRemoteUser(ctx, remoteUserID, "test", "test") assert.NoError(t, err) assert.Equal(t, localUserID, foundUserID) }) } func TestQueryWrap(t *testing.T) { tests := []struct { name string in string want string }{ {"plain placeholders", "SELECT a FROM t WHERE b = ? AND c = ?", "SELECT a FROM t WHERE b = $1 AND c = $2"}, {"placeholder inside single quotes", "SELECT '?' FROM t WHERE b = ?", "SELECT '?' FROM t WHERE b = $1"}, {"backtick inside single quotes", "SELECT '`' FROM t WHERE b = ?", "SELECT '`' FROM t WHERE b = $1"}, {"single quote inside backticks", "SELECT `it's` FROM t WHERE b = ?", "SELECT `it's` FROM t WHERE b = $1"}, {"placeholder inside double quotes", "SELECT \"a?b\" FROM t WHERE b = ?", "SELECT \"a?b\" FROM t WHERE b = $1"}, {"doubled single quote", "SELECT 'it''s ?' FROM t WHERE b = ?", "SELECT 'it''s ?' FROM t WHERE b = $1"}, } pg := &datastore{driverName: driverPostgres} other := &datastore{driverName: "sqlite3"} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := pg.QueryWrap(tt.in); got != tt.want { t.Errorf("postgres QueryWrap(%q) = %q, want %q", tt.in, got, tt.want) } if got := other.QueryWrap(tt.in); got != tt.in { t.Errorf("non-postgres QueryWrap(%q) = %q, want unchanged", tt.in, got) } }) } } + +func TestPostgresErrorChecksUnwrap(t *testing.T) { + db := &datastore{driverName: driverPostgres} + wrap := func(code pq.ErrorCode) error { + return fmt.Errorf("context: %w", &pq.Error{Code: code}) + } + + if !db.isDuplicateKeyErr(wrap(postgresErrDuplicateKey)) { + t.Error("isDuplicateKeyErr: wrapped unique_violation not recognised") + } + if !db.isHighLoadError(wrap(postgresErrTooManyConns)) { + t.Error("isHighLoadError: wrapped too_many_connections not recognised") + } + if db.isDuplicateKeyErr(wrap("42601")) { + t.Error("isDuplicateKeyErr: syntax_error recognised as a duplicate") + } +}