mysql-haskell-1.5.0: test/ParameterEscaping.hs
-- | Values that 'query', 'execute' and 'executeMany' render into the query
-- text, read back under the default sql_mode and under NO_BACKSLASH_ESCAPES.
-- In that mode a backslash is an ordinary character, so a quote escaped as
-- backslash-quote used to end the string literal and let the rest of the
-- value run as SQL.
module ParameterEscaping (tests) where
import qualified Data.ByteString as B
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word8)
import Database.MySQL.Base
import QueryApi
import qualified System.IO.Streams as Stream
import Test.Tasty
import Test.Tasty.HUnit
import TestUser (withTestConnection)
tests :: QueryApi -> TestTree
tests api = testGroup "parameters rendered into the query text"
[ testGroup "NO_BACKSLASH_ESCAPES"
[ testCase "a quote in a query parameter does not end the literal" $
withUsers $ \c -> do
(_, rows) <- apiQuery api (ColumnCount 2) c
"SELECT name, secret FROM escaping_users WHERE name = ?"
[One (MySQLText endsLiteral)]
found <- Stream.toList rows
assertEqual "rows matching the name" [] found
, testCase "a quote in an executeMany parameter does not end the literal" $
withUsers $ \c -> do
oks <- executeMany c
"UPDATE escaping_users SET secret = 'changed' WHERE name = ?"
[[MySQLText endsLiteral]]
assertEqual "rows changed" 0 (sum (map okAffectedRows oks))
, testCase "text with quotes and control characters round-trips" $
withTestConnection $ \c -> do
setNoBackslashEscapes c
assertTextRoundTrip api c (T.filter (/= '\\') trickyText)
, testCase "every byte but the backslash round-trips" $
withTestConnection $ \c -> do
setNoBackslashEscapes c
assertBytesRoundTrip api c (B.filter (/= backslash) everyByte)
]
, testGroup "default sql_mode"
[ testCase "text with quotes, backslashes and control characters round-trips" $
withTestConnection $ \c -> assertTextRoundTrip api c trickyText
, testCase "every byte round-trips" $
withTestConnection $ \c -> assertBytesRoundTrip api c everyByte
]
]
-- | A name that matches every row once its quote ends the string literal.
endsLiteral :: Text
endsLiteral = "nobody' OR 1=1 #"
-- | Ends in a backslash, which in the default mode escapes the closing quote
-- unless it is escaped itself.
trickyText :: Text
trickyText = "it's \"quoted\" '' \\' \\\\ \0 \n \r \t \b \SUB % _ \252nic\246de \128512 \\"
everyByte :: B.ByteString
everyByte = B.pack [minBound .. maxBound]
backslash :: Word8
backslash = 0x5C
-- | A connection under NO_BACKSLASH_ESCAPES with one row in @escaping_users@
-- whose name is not 'endsLiteral'.
withUsers :: (MySQLConn -> IO a) -> IO a
withUsers run = withTestConnection $ \c -> do
_ <- execute_ c "CREATE TEMPORARY TABLE escaping_users (name VARCHAR(64), secret VARCHAR(64))"
_ <- execute_ c "INSERT INTO escaping_users VALUES ('alice', 'hunter2')"
setNoBackslashEscapes c
run c
setNoBackslashEscapes :: MySQLConn -> IO ()
setNoBackslashEscapes c = do
_ <- execute_ c "SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES'"
pure ()
-- | Inserts with 'execute' and selects with the value in the WHERE clause, so
-- both the stored value and the literal compared against it must be exact.
assertTextRoundTrip :: QueryApi -> MySQLConn -> Text -> Assertion
assertTextRoundTrip api c text = do
_ <- execute_ c "CREATE TEMPORARY TABLE escaping_text (value VARCHAR(255) CHARACTER SET utf8mb4)"
_ <- execute c "INSERT INTO escaping_text VALUES (?)" [MySQLText text]
(_, rows) <- apiQuery api (ColumnCount 1) c "SELECT value FROM escaping_text WHERE value = ?"
[One (MySQLText text)]
found <- Stream.toList rows
assertEqual "rows read back" [[MySQLText text]] found
assertBytesRoundTrip :: QueryApi -> MySQLConn -> B.ByteString -> Assertion
assertBytesRoundTrip api c bytes = do
_ <- execute_ c "CREATE TEMPORARY TABLE escaping_bytes (value VARBINARY(512))"
_ <- execute c "INSERT INTO escaping_bytes VALUES (?)" [MySQLBytes bytes]
(_, rows) <- apiQuery api (ColumnCount 1) c "SELECT value FROM escaping_bytes WHERE value = ?"
[One (MySQLBytes bytes)]
found <- Stream.toList rows
assertEqual "rows read back" [[MySQLBytes bytes]] found