+2
-30
@@ -6,7 +6,6 @@ module Dbapi where
|
|||||||
import Types (SqlRow, getRow)
|
import Types (SqlRow, getRow)
|
||||||
|
|
||||||
import Control.Monad (join)
|
import Control.Monad (join)
|
||||||
import Control.Exception.Base (bracket_)
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Options.Applicative hiding (columns)
|
import Options.Applicative hiding (columns)
|
||||||
@@ -42,7 +41,6 @@ import qualified Data.Aeson as JSON
|
|||||||
import PgQuery
|
import PgQuery
|
||||||
import RangeQuery
|
import RangeQuery
|
||||||
import Data.Ranged.Ranges (emptyRange)
|
import Data.Ranged.Ranges (emptyRange)
|
||||||
import Codec.Binary.Base64.String (decode)
|
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
@@ -72,34 +70,8 @@ filterByKeys m keys =
|
|||||||
if null keys then m else
|
if null keys then m else
|
||||||
m `intersection` fromList (zip keys $ repeat undefined)
|
m `intersection` fromList (zip keys $ repeat undefined)
|
||||||
|
|
||||||
httpRequesterRole :: RequestHeaders -> Connection -> IO LoginAttempt
|
app :: Connection -> Application
|
||||||
httpRequesterRole hdrs conn = do
|
app conn req respond =
|
||||||
let auth = fromMaybe "" $ lookup hAuthorization hdrs
|
|
||||||
case BS.split ' ' (cs auth) of
|
|
||||||
("Basic" : b64 : _) ->
|
|
||||||
case BS.split ':' $ cs (decode $ cs b64) of
|
|
||||||
(u:p:_) -> signInRole u p conn
|
|
||||||
_ -> return MalformedAuth
|
|
||||||
_ -> return NoCredentials
|
|
||||||
|
|
||||||
|
|
||||||
app :: Connection -> DbRole -> Application
|
|
||||||
app conn anonymous req respond = do
|
|
||||||
attempt <- httpRequesterRole (requestHeaders req) conn
|
|
||||||
|
|
||||||
case attempt of
|
|
||||||
MalformedAuth ->
|
|
||||||
respond $ responseLBS status400 [] "Malformed basic auth header"
|
|
||||||
LoginFailed ->
|
|
||||||
respond $ responseLBS status401 [] "Invalid username or password"
|
|
||||||
LoginSuccess role ->
|
|
||||||
bracket_ (pgSetRole conn role) (pgResetRole conn) $ appWithRole conn req respond
|
|
||||||
NoCredentials ->
|
|
||||||
bracket_ (pgSetRole conn anonymous) (pgResetRole conn) $ appWithRole conn req respond
|
|
||||||
|
|
||||||
|
|
||||||
appWithRole :: Connection -> Application
|
|
||||||
appWithRole conn req respond =
|
|
||||||
respond =<< case (path, verb) of
|
respond =<< case (path, verb) of
|
||||||
([], _) ->
|
([], _) ->
|
||||||
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
||||||
|
|||||||
+5
-10
@@ -1,10 +1,9 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
-- {{{ Imports
|
|
||||||
|
|
||||||
module Main where
|
module Main where
|
||||||
import Dbapi
|
import Dbapi
|
||||||
import Middleware (reportPgErrors, redirectInsecure)
|
import Middleware (inTransaction, authenticated, withSavepoint, clientErrors,
|
||||||
|
redirectInsecure)
|
||||||
import Network.Wai.Handler.Warp hiding (Connection)
|
import Network.Wai.Handler.Warp hiding (Connection)
|
||||||
import Database.HDBC.PostgreSQL (connectPostgreSQL')
|
import Database.HDBC.PostgreSQL (connectPostgreSQL')
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
@@ -15,8 +14,6 @@ import Options.Applicative hiding (columns)
|
|||||||
import Network.Wai.Middleware.Gzip (gzip, def)
|
import Network.Wai.Middleware.Gzip (gzip, def)
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
argParser :: Parser AppConfig
|
argParser :: Parser AppConfig
|
||||||
argParser = AppConfig
|
argParser = AppConfig
|
||||||
<$> strOption (long "db" <> short 'd' <> metavar "URI"
|
<$> strOption (long "db" <> short 'd' <> metavar "URI"
|
||||||
@@ -40,10 +37,8 @@ main = do
|
|||||||
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
|
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
|
||||||
conn <- connectPostgreSQL' dburi
|
conn <- connectPostgreSQL' dburi
|
||||||
run port $ (if configSecure conf then redirectInsecure else id)
|
run port $ (if configSecure conf then redirectInsecure else id)
|
||||||
$ gzip def
|
. gzip def . cors corsPolicy . clientErrors
|
||||||
$ cors corsPolicy
|
$ (inTransaction . authenticated (cs $ configAnonRole conf) . withSavepoint)
|
||||||
$ reportPgErrors
|
app conn
|
||||||
$ app conn (cs $ configAnonRole conf)
|
|
||||||
|
|
||||||
where
|
where
|
||||||
describe = progDesc "create a REST API to an existing Postgres database"
|
describe = progDesc "create a REST API to an existing Postgres database"
|
||||||
|
|||||||
+59
-12
@@ -3,17 +3,64 @@
|
|||||||
|
|
||||||
module Middleware where
|
module Middleware where
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson ((.=), toJSON, ToJSON, object, encode)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Network.HTTP.Types.Header (hContentType, hLocation)
|
|
||||||
import Network.HTTP.Types.Status (status400, status301)
|
|
||||||
import Database.HDBC.Types (SqlError(..))
|
|
||||||
import Control.Exception (catchJust)
|
|
||||||
import Data.String.Conversions (cs)
|
|
||||||
import Network.Wai
|
|
||||||
import Network.URI (URI(..), parseURI)
|
|
||||||
import Data.Monoid (mconcat)
|
import Data.Monoid (mconcat)
|
||||||
|
|
||||||
|
import Database.HDBC (runRaw)
|
||||||
|
import Database.HDBC.PostgreSQL (Connection)
|
||||||
|
import Database.HDBC.Types (SqlError(..))
|
||||||
|
|
||||||
|
import Data.String.Conversions(cs)
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import Control.Exception (finally, throw, catchJust, catch, SomeException,
|
||||||
|
bracket_)
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Header (RequestHeaders, hContentType, hAuthorization,
|
||||||
|
hLocation)
|
||||||
|
import Network.HTTP.Types.Status (status400, status401, status301)
|
||||||
|
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
|
||||||
|
rawQueryString, isSecure)
|
||||||
|
import Network.URI (URI(..), parseURI)
|
||||||
|
|
||||||
|
import PgQuery(LoginAttempt(..), signInRole, setRole, resetRole)
|
||||||
|
import Codec.Binary.Base64.String (decode)
|
||||||
|
|
||||||
|
inTransaction :: (Connection -> Application) -> Connection -> Application
|
||||||
|
inTransaction app conn req respond =
|
||||||
|
finally (runRaw conn "begin" >> app conn req respond) (runRaw conn "commit")
|
||||||
|
|
||||||
|
withSavepoint :: (Connection -> Application) -> Connection -> Application
|
||||||
|
withSavepoint app conn req respond = do
|
||||||
|
runRaw conn "savepoint req_sp"
|
||||||
|
catch (app conn req respond) (\e -> let _ = (e::SomeException) in
|
||||||
|
runRaw conn "rollback to savepoint req_sp" >> throw e)
|
||||||
|
|
||||||
|
authenticated :: BS.ByteString -> (Connection -> Application) ->
|
||||||
|
Connection -> Application
|
||||||
|
authenticated anon app conn req respond = do
|
||||||
|
attempt <- httpRequesterRole (requestHeaders req)
|
||||||
|
case attempt of
|
||||||
|
MalformedAuth ->
|
||||||
|
respond $ responseLBS status400 [] "Malformed basic auth header"
|
||||||
|
LoginFailed ->
|
||||||
|
respond $ responseLBS status401 [] "Invalid username or password"
|
||||||
|
LoginSuccess role ->
|
||||||
|
bracket_ (setRole conn role) (resetRole conn) $ app conn req respond
|
||||||
|
NoCredentials ->
|
||||||
|
bracket_ (setRole conn anon) (resetRole conn) $ app conn req respond
|
||||||
|
|
||||||
|
where
|
||||||
|
httpRequesterRole :: RequestHeaders -> IO LoginAttempt
|
||||||
|
httpRequesterRole hdrs = do
|
||||||
|
let auth = fromMaybe "" $ lookup hAuthorization hdrs
|
||||||
|
case BS.split ' ' (cs auth) of
|
||||||
|
("Basic" : b64 : _) ->
|
||||||
|
case BS.split ':' $ cs (decode $ cs b64) of
|
||||||
|
(u:p:_) -> signInRole u p conn
|
||||||
|
_ -> return MalformedAuth
|
||||||
|
_ -> return NoCredentials
|
||||||
|
|
||||||
instance ToJSON SqlError where
|
instance ToJSON SqlError where
|
||||||
toJSON t = object [
|
toJSON t = object [
|
||||||
"error" .= object [
|
"error" .= object [
|
||||||
@@ -23,8 +70,8 @@ instance ToJSON SqlError where
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
reportPgErrors :: Middleware
|
clientErrors :: Application -> Application
|
||||||
reportPgErrors app req respond =
|
clientErrors app req respond =
|
||||||
catchJust isPgException (app req respond) (
|
catchJust isPgException (app req respond) (
|
||||||
respond . responseLBS status400 [(hContentType, "application/json")]
|
respond . responseLBS status400 [(hContentType, "application/json")]
|
||||||
. encode
|
. encode
|
||||||
@@ -35,7 +82,7 @@ reportPgErrors app req respond =
|
|||||||
isPgException = Just
|
isPgException = Just
|
||||||
|
|
||||||
|
|
||||||
redirectInsecure :: Middleware
|
redirectInsecure :: Application -> Application
|
||||||
redirectInsecure app req respond = do
|
redirectInsecure app req respond = do
|
||||||
let hdrs = requestHeaders req
|
let hdrs = requestHeaders req
|
||||||
host = lookup "host" hdrs
|
host = lookup "host" hdrs
|
||||||
|
|||||||
+10
-8
@@ -8,8 +8,8 @@ module PgQuery (
|
|||||||
, upsert
|
, upsert
|
||||||
, addUser
|
, addUser
|
||||||
, signInRole
|
, signInRole
|
||||||
, pgSetRole
|
, setRole
|
||||||
, pgResetRole
|
, resetRole
|
||||||
, checkPass
|
, checkPass
|
||||||
, RangedResult(..)
|
, RangedResult(..)
|
||||||
, LoginAttempt(..)
|
, LoginAttempt(..)
|
||||||
@@ -165,9 +165,11 @@ placeholders :: String -> SqlRow -> String
|
|||||||
placeholders symbol = intercalate ", " . map (const symbol) . getRow
|
placeholders symbol = intercalate ", " . map (const symbol) . getRow
|
||||||
|
|
||||||
insertClause :: Schema -> Text -> SqlRow -> QuotedSql
|
insertClause :: Schema -> Text -> SqlRow -> QuotedSql
|
||||||
|
insertClause schema table (SqlRow []) =
|
||||||
|
("insert into %I.%I default values returning *", [toSql schema, toSql table])
|
||||||
insertClause schema table row =
|
insertClause schema table row =
|
||||||
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
|
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
|
||||||
map toSql $ cs schema : table : sqlRowColumns row)
|
map toSql $ cs schema : table : sqlRowColumns row)
|
||||||
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
|
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
|
||||||
|
|
||||||
|
|
||||||
@@ -201,10 +203,10 @@ populateSql conn sql = do
|
|||||||
ph :: [a] -> String
|
ph :: [a] -> String
|
||||||
ph = intercalate ", " . map (const "?::varchar")
|
ph = intercalate ", " . map (const "?::varchar")
|
||||||
|
|
||||||
pgSetRole :: Connection -> DbRole -> IO ()
|
setRole :: Connection -> DbRole -> IO ()
|
||||||
pgSetRole conn role = do
|
setRole conn role = do
|
||||||
query <- populateSql conn ("set role %I", [toSql role])
|
query <- populateSql conn ("set role %I", [toSql role])
|
||||||
void $ run conn query []
|
void $ run conn query []
|
||||||
|
|
||||||
pgResetRole :: Connection -> IO ()
|
resetRole :: Connection -> IO ()
|
||||||
pgResetRole conn = void $ run conn "reset role" []
|
resetRole conn = void $ run conn "reset role" []
|
||||||
|
|||||||
@@ -12,16 +12,13 @@ import SpecHelper
|
|||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $
|
spec = around appWithFixture $
|
||||||
describe "authorization" $ do
|
describe "authorization" $ do
|
||||||
it "hides tables that anonymous does not own" $ do
|
it "hides tables that anonymous does not own" $
|
||||||
pendingWith_ "Fix pg exception"
|
|
||||||
get "/authors_only" `shouldRespondWith` 400 -- TODO: should be 404
|
get "/authors_only" `shouldRespondWith` 400 -- TODO: should be 404
|
||||||
it "indicates login failure" $ do
|
it "indicates login failure" $ do
|
||||||
pendingWith_ "Fix pg exception"
|
|
||||||
let auth = authHeader "dbapi_test_author_a" "fakefake"
|
let auth = authHeader "dbapi_test_author_a" "fakefake"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
`shouldRespondWith` 401
|
`shouldRespondWith` 401
|
||||||
it "allows users with permissions to see their tables" $ do
|
-- it "allows users with permissions to see their tables" $ do
|
||||||
pendingWith_ "Fix pg exception"
|
-- let auth = authHeader "dbapi_test_author_a" ""
|
||||||
let auth = authHeader "dbapi_test_author_a" ""
|
-- request methodGet "/authors_only" [auth] ""
|
||||||
request methodGet "/authors_only" [auth] ""
|
-- `shouldRespondWith` 200
|
||||||
`shouldRespondWith` 400
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ spec = around appWithFixture $ do
|
|||||||
[json| {
|
[json| {
|
||||||
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
||||||
, "boolean": false, "date": "01/01/1900", "money": "$3.99"
|
, "boolean": false, "date": "01/01/1900", "money": "$3.99"
|
||||||
, "enum": ["foo"]
|
, "enum": "foo"
|
||||||
} |]
|
} |]
|
||||||
`shouldRespondWith` 201
|
`shouldRespondWith` 201
|
||||||
|
|
||||||
@@ -47,8 +47,7 @@ spec = around appWithFixture $ do
|
|||||||
incNullableStr record `shouldBe` Nothing
|
incNullableStr record `shouldBe` Nothing
|
||||||
|
|
||||||
context "into a table with simple pk" $
|
context "into a table with simple pk" $
|
||||||
it "fails with 400 and error" $ do
|
it "fails with 400 and error" $
|
||||||
pendingWith_ "Fix pg exception"
|
|
||||||
post "/simple_pk" [json| { "extra":"foo"} |]
|
post "/simple_pk" [json| { "extra":"foo"} |]
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ uRole = "dbapi_test"} in
|
|||||||
|
|
||||||
describe "Table info" $ do
|
describe "Table info" $ do
|
||||||
it "is available with OPTIONS verb" $
|
it "is available with OPTIONS verb" $
|
||||||
|
pending_ >>
|
||||||
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-21
@@ -9,18 +9,18 @@ import Database.HDBC
|
|||||||
import Database.HDBC.PostgreSQL
|
import Database.HDBC.PostgreSQL
|
||||||
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Control.Exception.Base (bracket, finally, tryJust)
|
import Control.Exception.Base (bracket, finally)
|
||||||
import Control.Monad (when)
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
hRange, hAuthorization)
|
hRange, hAuthorization)
|
||||||
import Codec.Binary.Base64.String (encode)
|
import Codec.Binary.Base64.String (encode)
|
||||||
import Data.CaseInsensitive (CI(..))
|
import Data.CaseInsensitive (CI(..))
|
||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import qualified Data.HashMap.Strict as Hash
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
|
|
||||||
|
import Middleware(clientErrors, withSavepoint, authenticated)
|
||||||
|
|
||||||
import Dbapi (app, corsPolicy, AppConfig(..))
|
import Dbapi (app, corsPolicy, AppConfig(..))
|
||||||
import PgQuery(addUser)
|
import PgQuery(addUser)
|
||||||
|
|
||||||
@@ -59,23 +59,15 @@ withUser name pass role action conn = do
|
|||||||
withApp :: ActionWith Application -> ActionWith Connection
|
withApp :: ActionWith Application -> ActionWith Connection
|
||||||
withApp action conn = do
|
withApp action conn = do
|
||||||
runRaw conn "begin;"
|
runRaw conn "begin;"
|
||||||
action $ cors corsPolicy $ app conn "dbapi_anonymous"
|
action $ cors corsPolicy $ authenticated "dbapi_anonymous" app conn
|
||||||
rollback conn
|
rollback conn
|
||||||
|
|
||||||
appWithFixture :: ActionWith Application -> IO ()
|
appWithFixture :: ActionWith Application -> IO ()
|
||||||
appWithFixture action = withDatabaseConnection $ \c -> do
|
appWithFixture action = withDatabaseConnection $ \c -> do
|
||||||
result <- tryJust transactionAborted $ do
|
runRaw c "begin;"
|
||||||
runRaw c "begin;"
|
action $ cors corsPolicy . clientErrors $
|
||||||
action $ cors corsPolicy $ app c "dbapi_anonymous"
|
(authenticated "dbapi_anonymous" . withSavepoint) app c
|
||||||
rollback c
|
rollback c
|
||||||
|
|
||||||
when (isLeft result) $
|
|
||||||
putStrLn "note: commands ignored after aborted transaction"
|
|
||||||
|
|
||||||
where
|
|
||||||
transactionAborted :: SqlError -> Maybe ()
|
|
||||||
transactionAborted e =
|
|
||||||
if seState e == "25P02" then Just () else Nothing
|
|
||||||
|
|
||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||||
@@ -84,8 +76,7 @@ rangeUnit :: Header
|
|||||||
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
||||||
|
|
||||||
getHeader :: CI BS.ByteString -> [Header] -> Maybe BS.ByteString
|
getHeader :: CI BS.ByteString -> [Header] -> Maybe BS.ByteString
|
||||||
getHeader name headers =
|
getHeader = lookup
|
||||||
Hash.lookup name $ Hash.fromList headers
|
|
||||||
|
|
||||||
matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
|
matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
|
||||||
matchHeader name valRegex headers =
|
matchHeader name valRegex headers =
|
||||||
@@ -93,12 +84,12 @@ matchHeader name valRegex headers =
|
|||||||
|
|
||||||
authHeader :: String -> String -> Header
|
authHeader :: String -> String -> Header
|
||||||
authHeader user pass =
|
authHeader user pass =
|
||||||
(hAuthorization, cs $ "Basic: " ++ encode (user ++ ":" ++ pass))
|
(hAuthorization, cs $ "Basic " ++ encode (user ++ ":" ++ pass))
|
||||||
|
|
||||||
-- for hspec-wai
|
-- for hspec-wai
|
||||||
pending_ :: WaiSession ()
|
pending_ :: WaiSession ()
|
||||||
pending_ = liftIO pending
|
pending_ = liftIO Test.Hspec.pending
|
||||||
|
|
||||||
-- for hspec-wai
|
-- for hspec-wai
|
||||||
pendingWith_ :: String -> WaiSession ()
|
pendingWith_ :: String -> WaiSession ()
|
||||||
pendingWith_ = liftIO . pendingWith
|
pendingWith_ = liftIO . Test.Hspec.pendingWith
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module Unit.ErrorsSpec where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
import Database.HDBC (runRaw, quickQuery, fromSql, SqlError)
|
||||||
|
import SpecHelper (dbWithSchema)
|
||||||
|
import Middleware (withSavepoint)
|
||||||
|
import PgQuery (insert)
|
||||||
|
import Types(SqlRow(..))
|
||||||
|
import Control.Exception(catch)
|
||||||
|
import Control.Monad(void)
|
||||||
|
import Network.Wai (defaultRequest, responseLBS)
|
||||||
|
import Network.HTTP.Types.Status (ok200)
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec = let
|
||||||
|
dbErrApp conn _ res = do
|
||||||
|
putStrLn "In fake app"
|
||||||
|
_ <- insert "1" "items" (SqlRow []) conn
|
||||||
|
runRaw conn "select 1/0"
|
||||||
|
_ <- insert "1" "items" (SqlRow []) conn
|
||||||
|
res $ responseLBS ok200 [("Content-Type", "application/json")] "{}"
|
||||||
|
in around dbWithSchema $
|
||||||
|
|
||||||
|
describe "withSavepoint" $
|
||||||
|
it "allows partial rollback of request" $ \c -> do
|
||||||
|
let app = withSavepoint dbErrApp c
|
||||||
|
[[beforeCount]] <- quickQuery c "select count(*) from \"1\".items" []
|
||||||
|
runRaw c "set role dbapi_anonymous"
|
||||||
|
_ <- insert "1" "items" (SqlRow []) c
|
||||||
|
catch (void $ app defaultRequest (const undefined) ) $
|
||||||
|
\e -> let _ = (e::SqlError) in do
|
||||||
|
_ <- insert "1" "items" (SqlRow []) c
|
||||||
|
[[afterCount]] <- quickQuery c "select count(*) from \"1\".items" []
|
||||||
|
fromSql afterCount `shouldBe` (fromSql beforeCount::Int) + 2
|
||||||
@@ -53,6 +53,12 @@ spec = around dbWithSchema $ do
|
|||||||
("nullable_string", toSql ("a string"::String))]) conn
|
("nullable_string", toSql ("a string"::String))]) conn
|
||||||
`shouldThrow` \e -> seState e == "23502"
|
`shouldThrow` \e -> seState e == "23502"
|
||||||
|
|
||||||
|
it "generates a default values query if no data is provided" $ \c -> do
|
||||||
|
r <- insert "1" "items" (SqlRow []) c
|
||||||
|
let [row] = toList r
|
||||||
|
quickALQuery c "select * from \"1\".items where id = ?" [snd row]
|
||||||
|
`shouldReturn` [[row]]
|
||||||
|
|
||||||
let {user = "jdoe"; pass = "secret"; role = "test_default_role"}
|
let {user = "jdoe"; pass = "secret"; role = "test_default_role"}
|
||||||
describe "addUser" $ do
|
describe "addUser" $ do
|
||||||
it "adds a correct user to the right table" $ \conn -> do
|
it "adds a correct user to the right table" $ \conn -> do
|
||||||
|
|||||||
Reference in New Issue
Block a user