Add support for HEAD request
This commit is contained in:
committed by
Steve Chávez
parent
68cbe34c11
commit
620721dea7
@@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- #1383, Add support for HEAD request - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
## [6.0.2] - 2019-08-22
|
## [6.0.2] - 2019-08-22
|
||||||
|
|||||||
+36
-18
@@ -6,6 +6,7 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
|
|||||||
|
|
||||||
module PostgREST.ApiRequest (
|
module PostgREST.ApiRequest (
|
||||||
ApiRequest(..)
|
ApiRequest(..)
|
||||||
|
, InvokeMethod(..)
|
||||||
, ContentType(..)
|
, ContentType(..)
|
||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
@@ -51,11 +52,12 @@ import Protolude
|
|||||||
|
|
||||||
type RequestBody = BL.ByteString
|
type RequestBody = BL.ByteString
|
||||||
|
|
||||||
|
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
||||||
-- | Types of things a user wants to do to tables/views/procs
|
-- | Types of things a user wants to do to tables/views/procs
|
||||||
data Action = ActionCreate | ActionRead
|
data Action = ActionCreate | ActionRead{isHead :: Bool}
|
||||||
| ActionUpdate | ActionDelete
|
| ActionUpdate | ActionDelete
|
||||||
| ActionInfo | ActionInvoke{isReadOnly :: Bool}
|
| ActionSingleUpsert | ActionInvoke InvokeMethod
|
||||||
| ActionInspect | ActionSingleUpsert
|
| ActionInfo | ActionInspect{isHead :: Bool}
|
||||||
deriving Eq
|
deriving Eq
|
||||||
-- | The target db object of a user action
|
-- | The target db object of a user action
|
||||||
data Target = TargetIdent QualifiedIdentifier
|
data Target = TargetIdent QualifiedIdentifier
|
||||||
@@ -117,7 +119,7 @@ data ApiRequest = ApiRequest {
|
|||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userApiRequest :: Schema -> Maybe QualifiedIdentifier -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
userApiRequest :: Schema -> Maybe QualifiedIdentifier -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||||
userApiRequest schema rootSpec req reqBody
|
userApiRequest schema rootSpec req reqBody
|
||||||
| isTargetingProc && method `notElem` ["GET", "POST"] = Left ActionInappropriate
|
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
||||||
| topLevelRange == emptyRange = Left InvalidRange
|
| topLevelRange == emptyRange = Left InvalidRange
|
||||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
||||||
| otherwise = Right ApiRequest {
|
| otherwise = Right ApiRequest {
|
||||||
@@ -152,8 +154,10 @@ userApiRequest schema rootSpec req reqBody
|
|||||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||||
(filters, rpcQParams) =
|
(filters, rpcQParams) =
|
||||||
case action of
|
case action of
|
||||||
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
ActionInvoke InvGet -> partitionFlts
|
||||||
_ -> (flts, [])
|
ActionInvoke InvHead -> partitionFlts
|
||||||
|
_ -> (flts, [])
|
||||||
|
partitionFlts = partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
||||||
flts =
|
flts =
|
||||||
[ (toS k, toS $ fromJust v) |
|
[ (toS k, toS $ fromJust v) |
|
||||||
(k,v) <- qParams, isJust v,
|
(k,v) <- qParams, isJust v,
|
||||||
@@ -167,12 +171,13 @@ userApiRequest schema rootSpec req reqBody
|
|||||||
TargetProc _ _ -> True
|
TargetProc _ _ -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||||
columns | action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] = toS <$> join (lookup "columns" qParams)
|
columns
|
||||||
| otherwise = Nothing
|
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||||
|
| otherwise = Nothing
|
||||||
payload =
|
payload =
|
||||||
case (contentType, action) of
|
case (contentType, action) of
|
||||||
(_, ActionInvoke{isReadOnly=True}) ->
|
(_, ActionInvoke InvGet) -> Right rpcPrmsToJson
|
||||||
Right $ ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
|
(_, ActionInvoke InvHead) -> Right rpcPrmsToJson
|
||||||
(CTApplicationJSON, _) ->
|
(CTApplicationJSON, _) ->
|
||||||
if isJust columns
|
if isJust columns
|
||||||
then Right $ RawJSON reqBody
|
then Right $ RawJSON reqBody
|
||||||
@@ -189,21 +194,27 @@ userApiRequest schema rootSpec req reqBody
|
|||||||
Right $ ProcessedJSON (JSON.encode json) PJObject keys
|
Right $ ProcessedJSON (JSON.encode json) PJObject keys
|
||||||
(ct, _) ->
|
(ct, _) ->
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
||||||
|
rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams)
|
||||||
|
PJObject (S.fromList $ fst <$> rpcQParams)
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
"GET" | target == TargetDefaultSpec -> ActionInspect
|
-- The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response
|
||||||
| isTargetingProc -> ActionInvoke{isReadOnly=True}
|
-- From https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
||||||
| otherwise -> ActionRead
|
"HEAD" | target == TargetDefaultSpec -> ActionInspect{isHead=True}
|
||||||
|
| isTargetingProc -> ActionInvoke InvHead
|
||||||
|
| otherwise -> ActionRead{isHead=True}
|
||||||
|
"GET" | target == TargetDefaultSpec -> ActionInspect{isHead=False}
|
||||||
|
| isTargetingProc -> ActionInvoke InvGet
|
||||||
|
| otherwise -> ActionRead{isHead=False}
|
||||||
"POST" -> if isTargetingProc
|
"POST" -> if isTargetingProc
|
||||||
then ActionInvoke{isReadOnly=False}
|
then ActionInvoke InvPost
|
||||||
else ActionCreate
|
else ActionCreate
|
||||||
"PATCH" -> ActionUpdate
|
"PATCH" -> ActionUpdate
|
||||||
"PUT" -> ActionSingleUpsert
|
"PUT" -> ActionSingleUpsert
|
||||||
"DELETE" -> ActionDelete
|
"DELETE" -> ActionDelete
|
||||||
"OPTIONS" -> ActionInfo
|
"OPTIONS" -> ActionInfo
|
||||||
_ -> ActionInspect
|
_ -> ActionInspect{isHead=False}
|
||||||
target = case path of
|
target = case path of
|
||||||
[] -> case rootSpec of
|
[] -> case rootSpec of
|
||||||
Just rsQi -> TargetProc rsQi True
|
Just rsQi -> TargetProc rsQi True
|
||||||
@@ -212,7 +223,14 @@ userApiRequest schema rootSpec req reqBody
|
|||||||
["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False
|
["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False
|
||||||
other -> TargetUnknown other
|
other -> TargetUnknown other
|
||||||
|
|
||||||
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}]
|
shouldParsePayload =
|
||||||
|
action `elem`
|
||||||
|
[ActionCreate, ActionUpdate, ActionSingleUpsert,
|
||||||
|
ActionInvoke InvPost,
|
||||||
|
-- Though ActionInvoke{isGet=True}(a GET /rpc/..) doesn't really have a payload, we use the payload variable as a way
|
||||||
|
-- to store the query string arguments to the function.
|
||||||
|
ActionInvoke InvGet,
|
||||||
|
ActionInvoke InvHead]
|
||||||
relevantPayload | shouldParsePayload = rightToMaybe payload
|
relevantPayload | shouldParsePayload = rightToMaybe payload
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
path = pathInfo req
|
path = pathInfo req
|
||||||
|
|||||||
+21
-16
@@ -41,6 +41,7 @@ import Network.Wai
|
|||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||||
ContentType (..),
|
ContentType (..),
|
||||||
|
InvokeMethod (..),
|
||||||
PreferRepresentation (..),
|
PreferRepresentation (..),
|
||||||
Target (..), mutuallyAgreeable,
|
Target (..), mutuallyAgreeable,
|
||||||
userApiRequest)
|
userApiRequest)
|
||||||
@@ -55,12 +56,12 @@ import PostgREST.Error (PgError (..), SimpleError (..),
|
|||||||
import PostgREST.Middleware
|
import PostgREST.Middleware
|
||||||
import PostgREST.OpenAPI
|
import PostgREST.OpenAPI
|
||||||
import PostgREST.Parsers (pRequestColumns)
|
import PostgREST.Parsers (pRequestColumns)
|
||||||
import PostgREST.QueryBuilder (callProc,
|
import PostgREST.QueryBuilder (callProc, createReadStatement,
|
||||||
createReadStatement,
|
|
||||||
createWriteStatement,
|
createWriteStatement,
|
||||||
requestToCountQuery,
|
requestToCountQuery,
|
||||||
requestToQuery)
|
requestToQuery)
|
||||||
import PostgREST.RangeQuery (allRange, contentRangeH, rangeStatusHeader)
|
import PostgREST.RangeQuery (allRange, contentRangeH,
|
||||||
|
rangeStatusHeader)
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
import Protolude hiding (Proxy, intercalate)
|
import Protolude hiding (Proxy, intercalate)
|
||||||
|
|
||||||
@@ -101,15 +102,16 @@ postgrest conf refDbStructure pool getTime worker =
|
|||||||
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
|
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
|
||||||
transactionMode proc action =
|
transactionMode proc action =
|
||||||
case action of
|
case action of
|
||||||
ActionRead -> HT.Read
|
ActionRead _ -> HT.Read
|
||||||
ActionInfo -> HT.Read
|
ActionInfo -> HT.Read
|
||||||
ActionInspect -> HT.Read
|
ActionInspect _ -> HT.Read
|
||||||
ActionInvoke{isReadOnly=False} ->
|
ActionInvoke InvGet -> HT.Read
|
||||||
|
ActionInvoke InvHead -> HT.Read
|
||||||
|
ActionInvoke InvPost ->
|
||||||
let v = maybe Volatile pdVolatility proc in
|
let v = maybe Volatile pdVolatility proc in
|
||||||
if v == Stable || v == Immutable
|
if v == Stable || v == Immutable
|
||||||
then HT.Read
|
then HT.Read
|
||||||
else HT.Write
|
else HT.Write
|
||||||
ActionInvoke{isReadOnly=True} -> HT.Read
|
|
||||||
_ -> HT.Write
|
_ -> HT.Write
|
||||||
|
|
||||||
app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response
|
app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response
|
||||||
@@ -119,7 +121,7 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
Right contentType ->
|
Right contentType ->
|
||||||
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
|
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
|
||||||
|
|
||||||
(ActionRead, TargetIdent qi, Nothing) ->
|
(ActionRead headersOnly, TargetIdent qi, Nothing) ->
|
||||||
let partsField = (,) <$> readSqlParts
|
let partsField = (,) <$> readSqlParts
|
||||||
<*> (binaryField contentType rawContentTypes =<< fldNames) in
|
<*> (binaryField contentType rawContentTypes =<< fldNames) in
|
||||||
case partsField of
|
case partsField of
|
||||||
@@ -135,7 +137,8 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
then errorResponseFor . singularityError $ queryTotal
|
then errorResponseFor . singularityError $ queryTotal
|
||||||
else responseLBS status
|
else responseLBS status
|
||||||
[toHeader contentType, contentRange,
|
[toHeader contentType, contentRange,
|
||||||
contentLocationH (qiName qi) (iCanonicalQS apiRequest)] (toS body)
|
contentLocationH (qiName qi) (iCanonicalQS apiRequest)]
|
||||||
|
(if headersOnly then mempty else toS body)
|
||||||
|
|
||||||
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||||
case mutateSqlParts tSchema tName of
|
case mutateSqlParts tSchema tName of
|
||||||
@@ -266,7 +269,7 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header in
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header in
|
||||||
return $ responseLBS status200 [allOrigins, allowH] mempty
|
return $ responseLBS status200 [allOrigins, allowH] mempty
|
||||||
|
|
||||||
(ActionInvoke _, TargetProc qi _, Just pJson) ->
|
(ActionInvoke invMethod, TargetProc qi _, Just pJson) ->
|
||||||
let returnsScalar = case proc of
|
let returnsScalar = case proc of
|
||||||
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
@@ -294,9 +297,11 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
then do
|
then do
|
||||||
HT.condemn
|
HT.condemn
|
||||||
return . errorResponseFor . singularityError $ queryTotal
|
return . errorResponseFor . singularityError $ queryTotal
|
||||||
else return $ responseLBS status ([toHeader contentType, contentRange] ++ toHeaders hs) (toS body)
|
else
|
||||||
|
return $ responseLBS status ([toHeader contentType, contentRange] ++ toHeaders hs)
|
||||||
|
(if invMethod == InvHead then mempty else toS body)
|
||||||
|
|
||||||
(ActionInspect, TargetDefaultSpec, Nothing) -> do
|
(ActionInspect headersOnly, TargetDefaultSpec, Nothing) -> do
|
||||||
let host = configHost conf
|
let host = configHost conf
|
||||||
port = toInteger $ configPort conf
|
port = toInteger $ configPort conf
|
||||||
proxy = pickProxy $ toS <$> configProxyUri conf
|
proxy = pickProxy $ toS <$> configProxyUri conf
|
||||||
@@ -308,7 +313,7 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
|
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
|
||||||
|
|
||||||
body <- encodeApi <$> H.statement schema accessibleTables <*> H.statement schema schemaDescription <*> H.statement schema accessibleProcs
|
body <- encodeApi <$> H.statement schema accessibleTables <*> H.statement schema schemaDescription <*> H.statement schema accessibleProcs
|
||||||
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
return $ responseLBS status200 [toHeader CTOpenAPI] (if headersOnly then mempty else toS body)
|
||||||
|
|
||||||
_ -> return notFound
|
_ -> return notFound
|
||||||
|
|
||||||
@@ -335,7 +340,7 @@ responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target
|
|||||||
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
|
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
|
||||||
where
|
where
|
||||||
contentTypesForRequest = case action of
|
contentTypesForRequest = case action of
|
||||||
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionRead _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
++ rawContentTypes
|
++ rawContentTypes
|
||||||
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
@@ -343,7 +348,7 @@ responseContentTypeOrError accepts rawContentTypes action target = serves conten
|
|||||||
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
++ rawContentTypes
|
++ rawContentTypes
|
||||||
++ [CTOpenAPI | tpIsRootSpec target]
|
++ [CTOpenAPI | tpIsRootSpec target]
|
||||||
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
|
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
|
||||||
ActionInfo -> [CTTextCSV]
|
ActionInfo -> [CTTextCSV]
|
||||||
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
serves sProduces cAccepts =
|
serves sProduces cAccepts =
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ readRequest maxRows allRels proc apiRequest =
|
|||||||
buildReadRequest :: [Tree SelectItem] -> ReadRequest
|
buildReadRequest :: [Tree SelectItem] -> ReadRequest
|
||||||
buildReadRequest fieldTree =
|
buildReadRequest fieldTree =
|
||||||
let rootDepth = 0
|
let rootDepth = 0
|
||||||
rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in
|
rootNodeName = case action of
|
||||||
|
ActionRead _ -> rootTableName
|
||||||
|
_ -> sourceCTEName in
|
||||||
foldr (treeEntry rootDepth) (Node (Select [] rootNodeName Nothing [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree
|
foldr (treeEntry rootDepth) (Node (Select [] rootNodeName Nothing [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree
|
||||||
where
|
where
|
||||||
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
||||||
@@ -278,7 +280,7 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
|||||||
(flts, logFrst) =
|
(flts, logFrst) =
|
||||||
case action of
|
case action of
|
||||||
ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest)
|
ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest)
|
||||||
ActionRead -> (iFilters apiRequest, iLogic apiRequest)
|
ActionRead _ -> (iFilters apiRequest, iLogic apiRequest)
|
||||||
_ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
|
_ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
|
||||||
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
||||||
orders = mapM pRequestOrder $ iOrder apiRequest
|
orders = mapM pRequestOrder $ iOrder apiRequest
|
||||||
|
|||||||
@@ -155,12 +155,17 @@ spec = do
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "limit and offset works on first level" $
|
it "limit and offset works on first level" $ do
|
||||||
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
||||||
`shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]
|
`shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "2-4/*"]
|
, matchHeaders = ["Content-Range" <:> "2-4/*"]
|
||||||
}
|
}
|
||||||
|
request methodHead "/items?select=id&order=id.asc&limit=3&offset=2" [] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "2-4/*"]
|
||||||
|
}
|
||||||
|
|
||||||
it "succeeds if offset equals 0 as a no-op" $
|
it "succeeds if offset equals 0 as a no-op" $
|
||||||
get "/items?select=id&offset=0"
|
get "/items?select=id&offset=0"
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ spec actualPgVersion =
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
request methodHead "/rpc/getitemrange?min=2&max=4"
|
||||||
|
(rangeHdrs (ByteRangeFromTo 0 0)) ""
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
|
}
|
||||||
|
|
||||||
it "includes total count if requested" $ do
|
it "includes total count if requested" $ do
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange"
|
||||||
@@ -49,6 +55,12 @@ spec actualPgVersion =
|
|||||||
{ matchStatus = 206
|
{ matchStatus = 206
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||||
}
|
}
|
||||||
|
request methodHead "/rpc/getitemrange?min=2&max=4"
|
||||||
|
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 206
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||||
|
}
|
||||||
|
|
||||||
it "returns proper json" $ do
|
it "returns proper json" $ do
|
||||||
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
||||||
@@ -72,6 +84,12 @@ spec actualPgVersion =
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
request methodHead "/rpc/getitemrange?min=2&max=4"
|
||||||
|
(acceptHdrs "text/csv") ""
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
context "unknown function" $ do
|
context "unknown function" $ do
|
||||||
it "returns 404" $
|
it "returns 404" $
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ spec :: SpecWith Application
|
|||||||
spec = do
|
spec = do
|
||||||
|
|
||||||
describe "OpenAPI" $ do
|
describe "OpenAPI" $ do
|
||||||
it "root path returns a valid openapi spec" $
|
it "root path returns a valid openapi spec" $ do
|
||||||
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
||||||
|
request methodHead "/" (acceptHdrs "application/openapi+json") ""
|
||||||
|
`shouldRespondWith` "" { matchStatus = 200 }
|
||||||
|
|
||||||
it "should respond to openapi request on none root path with 415" $
|
it "should respond to openapi request on none root path with 415" $
|
||||||
request methodGet "/items"
|
request methodGet "/items"
|
||||||
|
|||||||
Reference in New Issue
Block a user