feat: add Content-Length response header
This commit is contained in:
committed by
Steve Chavez
parent
98fcbedca5
commit
57ef9988a5
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ The selected columns in the embedded resources are aggregated into arrays
|
+ The selected columns in the embedded resources are aggregated into arrays
|
||||||
+ Aggregates are not supported
|
+ Aggregates are not supported
|
||||||
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
|
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
|
||||||
|
- #4016, Add `Content-Length` response header - @laurenceisla
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ Observability allows measuring a system's current state based on the data it gen
|
|||||||
Logs
|
Logs
|
||||||
====
|
====
|
||||||
|
|
||||||
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
|
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available.
|
||||||
|
|
||||||
With :ref:`log-level` set to ``info``, we get:
|
With :ref:`log-level` set to ``info``, we get:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
|
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 56 "" "curl/7.64.0"
|
||||||
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
|
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 162 "" "curl/7.64.0"
|
||||||
|
|
||||||
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
|
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ This will be logged by PostgREST:
|
|||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
|
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
|
||||||
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 - "" "curl/8.7.1"
|
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 99 "" "curl/8.7.1"
|
||||||
|
|
||||||
Database Logs
|
Database Logs
|
||||||
-------------
|
-------------
|
||||||
@@ -270,6 +270,27 @@ This header communicates metrics of the different phases in the request-response
|
|||||||
|
|
||||||
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
||||||
|
|
||||||
|
.. _content-length_header:
|
||||||
|
|
||||||
|
Content-Length Header
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
You can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl -i 'localhost:3000/users'
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Length: 104
|
||||||
|
|
||||||
|
Note that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).
|
||||||
|
This is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
||||||
|
|
||||||
|
The body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.
|
||||||
|
|
||||||
.. _explain_plan:
|
.. _explain_plan:
|
||||||
|
|
||||||
Execution plan
|
Execution plan
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ module PostgREST.Error
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.FuzzySet as Fuzzy
|
import qualified Data.FuzzySet as Fuzzy
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
@@ -59,8 +60,11 @@ class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
|||||||
|
|
||||||
errorResponseFor :: a -> Response
|
errorResponseFor :: a -> Response
|
||||||
errorResponseFor err =
|
errorResponseFor err =
|
||||||
let baseHeader = MediaType.toContentType MTApplicationJSON in
|
let
|
||||||
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
|
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||||
|
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||||
|
in
|
||||||
|
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
|
||||||
|
|
||||||
class ErrorBody a where
|
class ErrorBody a where
|
||||||
code :: a -> Text
|
code :: a -> Text
|
||||||
|
|||||||
+32
-15
@@ -68,6 +68,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
|
|||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
let
|
let
|
||||||
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
|
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody]
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||||
headers =
|
headers =
|
||||||
[ contentRange
|
[ contentRange
|
||||||
@@ -77,6 +78,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
|
|||||||
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
|
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
++ cLHeader
|
||||||
++ contentTypeHeaders wrMedia ctxApiRequest
|
++ contentTypeHeaders wrMedia ctxApiRequest
|
||||||
++ prefHeader
|
++ prefHeader
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
@@ -112,6 +114,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
|||||||
)
|
)
|
||||||
, Just . RangeQuery.contentRangeH 1 0 $
|
, Just . RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
|
, Just $ contentLengthHeaderStrict rsBody
|
||||||
, prefHeader ]
|
, prefHeader ]
|
||||||
|
|
||||||
let isInsertIfGTZero i =
|
let isInsertIfGTZero i =
|
||||||
@@ -130,7 +133,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
@@ -143,7 +146,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
|
|||||||
|
|
||||||
let (status, headers', body) =
|
let (status, headers', body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, headers, mempty)
|
Just None -> (HTTP.status204, headers, mempty)
|
||||||
_ -> (HTTP.status204, headers, mempty)
|
_ -> (HTTP.status204, headers, mempty)
|
||||||
|
|
||||||
@@ -152,19 +155,20 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||||
|
cLHeader = [contentLengthHeaderStrict rsBody]
|
||||||
cTHeader = contentTypeHeaders mrMedia ctxApiRequest
|
cTHeader = contentTypeHeaders mrMedia ctxApiRequest
|
||||||
|
|
||||||
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
|
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
|
||||||
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
|
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
|
||||||
(status, headers, body) =
|
(status, headers, body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (upsertStatus, cTHeader ++ prefHeader, LBS.fromStrict rsBody)
|
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, prefHeader, mempty)
|
Just None -> (HTTP.status204, prefHeader, mempty)
|
||||||
_ -> (HTTP.status204, prefHeader, mempty)
|
_ -> (HTTP.status204, prefHeader, mempty)
|
||||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
||||||
@@ -172,7 +176,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrM
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
@@ -185,7 +189,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
|
|||||||
|
|
||||||
let (status, headers', body) =
|
let (status, headers', body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, headers, mempty)
|
Just None -> (HTTP.status204, headers, mempty)
|
||||||
_ -> (HTTP.status204, headers, mempty)
|
_ -> (HTTP.status204, headers, mempty)
|
||||||
|
|
||||||
@@ -194,7 +198,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
@@ -205,7 +209,9 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
|
|||||||
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
|
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
|
||||||
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
else LBS.fromStrict rsBody
|
else LBS.fromStrict rsBody
|
||||||
|
isHeadMethod = invMethod == InvRead True
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
||||||
|
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
|
||||||
headers = contentRange : prefHeader
|
headers = contentRange : prefHeader
|
||||||
|
|
||||||
let (status', headers', body) =
|
let (status', headers', body) =
|
||||||
@@ -213,20 +219,22 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
|
|||||||
(HTTP.status204, headers, mempty)
|
(HTTP.status204, headers, mempty)
|
||||||
else
|
else
|
||||||
(status,
|
(status,
|
||||||
headers ++ contentTypeHeaders crMedia ctxApiRequest,
|
headers ++ cLHeader ++ contentTypeHeaders crMedia ctxApiRequest,
|
||||||
if invMethod == InvRead True then mempty else rsOrErrBody)
|
if isHeadMethod then mempty else rsOrErrBody)
|
||||||
|
|
||||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
|
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
|
||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
||||||
Right $ PgrstResponse HTTP.status200
|
let
|
||||||
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
|
||||||
(maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
|
cLHeader = if headersOnly then mempty else [contentLengthHeaderLazy rsBody]
|
||||||
|
in
|
||||||
|
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
|
||||||
|
|
||||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
||||||
case HM.lookup qi dbTables of
|
case HM.lookup qi dbTables of
|
||||||
@@ -251,7 +259,7 @@ actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GE
|
|||||||
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
||||||
respondInfo allowHeader =
|
respondInfo allowHeader =
|
||||||
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
||||||
Right $ PgrstResponse HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||||
|
|
||||||
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
|
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
|
||||||
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
|
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
|
||||||
@@ -268,6 +276,15 @@ decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
|||||||
decodeGucStatus =
|
decodeGucStatus =
|
||||||
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
||||||
|
|
||||||
|
contentLengthHeader :: Show b => (a -> b) -> a -> HTTP.Header
|
||||||
|
contentLengthHeader lenFn body = ("Content-Length", show (lenFn body))
|
||||||
|
|
||||||
|
contentLengthHeaderStrict :: BS.ByteString -> HTTP.Header
|
||||||
|
contentLengthHeaderStrict = contentLengthHeader BS.length
|
||||||
|
|
||||||
|
contentLengthHeaderLazy :: LBS.ByteString -> HTTP.Header
|
||||||
|
contentLengthHeaderLazy = contentLengthHeader LBS.length
|
||||||
|
|
||||||
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
|
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
|
||||||
contentTypeHeaders mediaType ApiRequest{..} =
|
contentTypeHeaders mediaType ApiRequest{..} =
|
||||||
MediaType.toContentType mediaType : maybeToList (profileHeader iSchema iNegotiatedByProfile)
|
MediaType.toContentType mediaType : maybeToList (profileHeader iSchema iNegotiatedByProfile)
|
||||||
|
|||||||
+9
-9
@@ -964,45 +964,45 @@ def test_log_level(level, defaultenv):
|
|||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
elif level == "error":
|
elif level == "error":
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
output[0],
|
output[0],
|
||||||
)
|
)
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
elif level == "warn":
|
elif level == "warn":
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
output[0],
|
output[0],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
output[1],
|
output[1],
|
||||||
)
|
)
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
elif level == "info":
|
elif level == "info":
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
output[0],
|
output[0],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 - "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
output[1],
|
output[1],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
output[2],
|
output[2],
|
||||||
)
|
)
|
||||||
assert len(output) == 3
|
assert len(output) == 3
|
||||||
elif level == "debug":
|
elif level == "debug":
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
output[0],
|
output[0],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 - "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
output[1],
|
output[1],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert re.match(
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
output[2],
|
output[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ spec = describe "authorization" $ do
|
|||||||
"code":"42501",
|
"code":"42501",
|
||||||
"message":"permission denied for table authors_only"} |]
|
"message":"permission denied for table authors_only"} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
|
, matchHeaders = [ "WWW-Authenticate" <:> "Bearer"
|
||||||
|
, "Content-Length" <:> "96" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "denies access to tables that postgrest_test_author does not own" $
|
it "denies access to tables that postgrest_test_author does not own" $
|
||||||
@@ -35,7 +36,7 @@ spec = describe "authorization" $ do
|
|||||||
"code":"42501",
|
"code":"42501",
|
||||||
"message":"permission denied for table private_table"} |]
|
"message":"permission denied for table private_table"} |]
|
||||||
{ matchStatus = 403
|
{ matchStatus = 403
|
||||||
, matchHeaders = []
|
, matchHeaders = ["Content-Length" <:> "97"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "denies execution on functions that anonymous does not own" $
|
it "denies execution on functions that anonymous does not own" $
|
||||||
@@ -92,7 +93,8 @@ spec = describe "authorization" $ do
|
|||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = [
|
, matchHeaders = [
|
||||||
"WWW-Authenticate" <:>
|
"WWW-Authenticate" <:>
|
||||||
"Bearer error=\"invalid_token\", error_description=\"Empty JWT is sent in Authorization header\""
|
"Bearer error=\"invalid_token\", error_description=\"Empty JWT is sent in Authorization header\"",
|
||||||
|
"Content-Length" <:> "100"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ spec =
|
|||||||
[("Accept","application/openapi+json")] ""
|
[("Accept","application/openapi+json")] ""
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST126","details":null,"hint":null,"message":"Root endpoint metadata is disabled"} |]
|
[json| {"code":"PGRST126","details":null,"hint":null,"message":"Root endpoint metadata is disabled"} |]
|
||||||
{ matchStatus = 404 }
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = ["Content-Length" <:> "93"]}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ spec = describe "OpenAPI Ignore Privileges" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8" ]
|
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8"
|
||||||
|
, matchHeaderAbsent hContentLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "table" $ do
|
describe "table" $ do
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ spec = describe "OpenAPI" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8"
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "should respond to openapi request on none root path with 406" $
|
it "should respond to openapi request on none root path with 406" $
|
||||||
@@ -38,11 +39,14 @@ spec = describe "OpenAPI" $ do
|
|||||||
`shouldRespondWith` 406
|
`shouldRespondWith` 406
|
||||||
|
|
||||||
it "includes postgrest.org current version api docs" $ do
|
it "includes postgrest.org current version api docs" $ do
|
||||||
r <- simpleBody <$> get "/"
|
r <- get "/"
|
||||||
|
|
||||||
let docsUrl = r ^? key "externalDocs" . key "url"
|
let headers = simpleHeaders r
|
||||||
|
docsUrl = simpleBody r ^? key "externalDocs" . key "url"
|
||||||
|
|
||||||
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
|
liftIO $ do
|
||||||
|
headers `shouldSatisfy` notZeroContentLength
|
||||||
|
docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
|
||||||
|
|
||||||
describe "schema" $ do
|
describe "schema" $ do
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ spec = describe "Allow header" $ do
|
|||||||
context "a table" $ do
|
context "a table" $ do
|
||||||
it "includes read/write methods for writeable table" $ do
|
it "includes read/write methods for writeable table" $ do
|
||||||
r <- request methodOptions "/items" [] ""
|
r <- request methodOptions "/items" [] ""
|
||||||
liftIO $
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy` matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "0"
|
||||||
|
|
||||||
it "fails with 404 for an unknown table" $
|
it "fails with 404 for an unknown table" $
|
||||||
request methodOptions "/unknown" [] "" `shouldRespondWith` 404
|
request methodOptions "/unknown" [] "" `shouldRespondWith` 404
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ spec = describe "custom media types" $ do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
|
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
|
||||||
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
|
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
|
||||||
|
simpleHeaders r `shouldContain` [("Content-Length", "30")]
|
||||||
|
|
||||||
it "can query by id if there's an aggregate defined for the table" $ do
|
it "can query by id if there's an aggregate defined for the table" $ do
|
||||||
r <- request methodGet "/lines?id=eq.1" (acceptHdrs "application/vnd.twkb") ""
|
r <- request methodGet "/lines?id=eq.1" (acceptHdrs "application/vnd.twkb") ""
|
||||||
@@ -32,7 +33,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |]
|
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Length" <:> "110" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can get raw xml output with Accept: text/xml if there's an aggregate defined" $ do
|
it "can get raw xml output with Accept: text/xml if there's an aggregate defined" $ do
|
||||||
@@ -40,7 +42,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"<myxml>foo</myxml>bar<foobar><baz/></foobar>"
|
"<myxml>foo</myxml>bar<foobar><baz/></foobar>"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "text/xml; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "44"]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO SOH (start of heading) is being added to results
|
-- TODO SOH (start of heading) is being added to results
|
||||||
@@ -50,7 +53,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
|
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
|
, matchHeaders = [ "Content-Type" <:> "application/vnd.geo2+json"
|
||||||
|
, "Content-Length" <:> "48" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "will use the more specific application/vnd.geo2 handler for this table" $ do
|
it "will use the more specific application/vnd.geo2 handler for this table" $ do
|
||||||
@@ -83,14 +87,16 @@ spec = describe "custom media types" $ do
|
|||||||
|</html>
|
|</html>
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/html"]
|
, matchHeaders = [ "Content-Type" <:> "text/html"
|
||||||
|
, "Content-Length" <:> "117" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can get raw output with Accept: text/plain" $ do
|
it "can get raw output with Accept: text/plain" $ do
|
||||||
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
|
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
|
||||||
`shouldRespondWith` "Welcome to PostgREST"
|
`shouldRespondWith` "Welcome to PostgREST"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "text/plain; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "20" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can get raw xml output with Accept: text/xml" $ do
|
it "can get raw xml output with Accept: text/xml" $ do
|
||||||
@@ -98,7 +104,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"<my-xml-tag/>"
|
"<my-xml-tag/>"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "text/xml; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "13" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can get raw xml output with Accept: text/xml" $ do
|
it "can get raw xml output with Accept: text/xml" $ do
|
||||||
@@ -132,6 +139,7 @@ spec = describe "custom media types" $ do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody r `shouldBe` readFixtureFile "A.png"
|
simpleBody r `shouldBe` readFixtureFile "A.png"
|
||||||
simpleHeaders r `shouldContain` [("Content-Type", "image/png")]
|
simpleHeaders r `shouldContain` [("Content-Type", "image/png")]
|
||||||
|
simpleHeaders r `shouldContain` [("Content-Length", "138")]
|
||||||
|
|
||||||
context "Proc that returns set of scalars and Accept: text/plain" $
|
context "Proc that returns set of scalars and Accept: text/plain" $
|
||||||
it "will err because only scalars work with media type domains" $ do
|
it "will err because only scalars work with media type domains" $ do
|
||||||
@@ -141,7 +149,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|]
|
[json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "110"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "Proc that returns rows and accepts custom media type" $ do
|
context "Proc that returns rows and accepts custom media type" $ do
|
||||||
@@ -173,7 +182,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"overridden": "true"} |]
|
[json| {"overridden": "true"} |]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "22" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO SOH (start of heading) is being added to results
|
-- TODO SOH (start of heading) is being added to results
|
||||||
@@ -182,7 +192,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"\SOH{\"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:4326\"}}, \"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"LineString\", \"coordinates\": [[1, 1], [5, 5]]}, \"properties\": {\"id\": 1, \"name\": \"line-1\"}}]}"
|
"\SOH{\"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:4326\"}}, \"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"LineString\", \"coordinates\": [[1, 1], [5, 5]]}, \"properties\": {\"id\": 1, \"name\": \"line-1\"}}]}"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "application/geo+json; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "239" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "will not override vendored media types like application/vnd.pgrst.object" $
|
it "will not override vendored media types like application/vnd.pgrst.object" $
|
||||||
@@ -190,7 +201,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"id":1,"name":"Windows 7","client_id":1}|]
|
[json|{"id":1,"name":"Windows 7","client_id":1}|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
|
, matchHeaders = [ "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
|
||||||
|
, "Content-Length" <:> "41" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "matches requested media type correctly" $ do
|
context "matches requested media type correctly" $ do
|
||||||
@@ -200,11 +212,13 @@ spec = describe "custom media types" $ do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody r1 `shouldBe` readFixtureFile "A.png"
|
simpleBody r1 `shouldBe` readFixtureFile "A.png"
|
||||||
simpleHeaders r1 `shouldContain` [("Content-Type", "image/png")]
|
simpleHeaders r1 `shouldContain` [("Content-Type", "image/png")]
|
||||||
|
simpleHeaders r1 `shouldContain` [("Content-Length", "138")]
|
||||||
|
|
||||||
r2 <- request methodGet "/rpc/ret_image" (acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8") ""
|
r2 <- request methodGet "/rpc/ret_image" (acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8") ""
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody r2 `shouldBe` readFixtureFile "A.png"
|
simpleBody r2 `shouldBe` readFixtureFile "A.png"
|
||||||
simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")]
|
simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")]
|
||||||
|
simpleHeaders r2 `shouldContain` [("Content-Length", "138")]
|
||||||
|
|
||||||
-- https://github.com/PostgREST/postgrest/issues/2170
|
-- https://github.com/PostgREST/postgrest/issues/2170
|
||||||
it "will match json in presence of text/plain" $ do
|
it "will match json in presence of text/plain" $ do
|
||||||
@@ -219,7 +233,8 @@ spec = describe "custom media types" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
|
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/tab-separated-values"]
|
, matchHeaders = [ "Content-Type" <:> "text/tab-separated-values"
|
||||||
|
, "Content-Length" <:> "47" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- https://github.com/PostgREST/postgrest/issues/1371#issuecomment-519248984
|
-- https://github.com/PostgREST/postgrest/issues/1371#issuecomment-519248984
|
||||||
@@ -229,6 +244,7 @@ spec = describe "custom media types" $ do
|
|||||||
simpleBody r `shouldBe` readFixtureFile "lines.csv"
|
simpleBody r `shouldBe` readFixtureFile "lines.csv"
|
||||||
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
|
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
|
||||||
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
|
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
|
||||||
|
simpleHeaders r `shouldContain` [("Content-Length", "216")]
|
||||||
|
|
||||||
-- https://github.com/PostgREST/postgrest/issues/3160
|
-- https://github.com/PostgREST/postgrest/issues/3160
|
||||||
context "using select query parameter" $ do
|
context "using select query parameter" $ do
|
||||||
@@ -239,7 +255,8 @@ spec = describe "custom media types" $ do
|
|||||||
|(2,"Windows 10",1)
|
|(2,"Windows 10",1)
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
|
, matchHeaders = [ "Content-Type" <:> "pg/outfunc"
|
||||||
|
, "Content-Length" <:> "37" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "with fewer columns selected" $ do
|
it "with fewer columns selected" $ do
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ spec =
|
|||||||
`shouldRespondWith` [json|[{"id":2}]|]
|
`shouldRespondWith` [json|[{"id":2}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/1"
|
, matchHeaders = ["Content-Range" <:> "*/1"
|
||||||
|
, "Content-Length" <:> "10"
|
||||||
, "Preference-Applied" <:> "return=representation, count=exact"]
|
, "Preference-Applied" <:> "return=representation, count=exact"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
request methodDelete "/items?id=eq.3&select=id"
|
request methodDelete "/items?id=eq.3&select=id"
|
||||||
@@ -50,6 +53,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*"
|
, "Content-Range" <:> "*/*"
|
||||||
, "Preference-Applied" <:> "return=minimal"]
|
, "Preference-Applied" <:> "return=minimal"]
|
||||||
}
|
}
|
||||||
@@ -58,7 +62,8 @@ spec =
|
|||||||
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
|
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
|
||||||
`shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]
|
`shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
, matchHeaders = [ "Content-Range" <:> "*/*"
|
||||||
|
, "Content-Length" <:> "23" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can rename and cast the selected columns" $
|
it "can rename and cast the selected columns" $
|
||||||
@@ -137,6 +142,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType
|
, matchHeaders = [matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Preference-Applied" <:> "return=minimal" ]
|
, "Preference-Applied" <:> "return=minimal" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +153,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with ordering" $
|
context "with ordering" $
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ spec =
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Length" <:> "828" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "errs on an ambiguous embed that has a circular reference" $
|
it "errs on an ambiguous embed that has a circular reference" $
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ pgErrorCodeMapping = do
|
|||||||
"details": null,
|
"details": null,
|
||||||
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.",
|
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.",
|
||||||
"message": "stack depth limit exceeded"}|]
|
"message": "stack depth limit exceeded"}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500
|
||||||
|
, matchHeaders = ["Content-Length" <:> "217"] }
|
||||||
|
|
||||||
context "includes the proxy-status header on the response" $ do
|
context "includes the proxy-status header on the response" $ do
|
||||||
it "works with ApiRequest error" $
|
it "works with ApiRequest error" $
|
||||||
@@ -34,7 +35,8 @@ pgErrorCodeMapping = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST125"]
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST125"
|
||||||
|
, "Content-Length" <:> "96" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works with SchemaCache error" $
|
it "works with SchemaCache error" $
|
||||||
@@ -42,7 +44,8 @@ pgErrorCodeMapping = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST205"]
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
||||||
|
, "Content-Length" <:> "172" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works with Jwt error" $ do
|
it "works with Jwt error" $ do
|
||||||
@@ -51,7 +54,8 @@ pgErrorCodeMapping = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
|
[json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
|
||||||
{ matchStatus = 401
|
{ matchStatus = 401
|
||||||
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST301"]
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST301"
|
||||||
|
, "Content-Length" <:> "89" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works with raise sqlstate custom error" $
|
it "works with raise sqlstate custom error" $
|
||||||
@@ -59,7 +63,8 @@ pgErrorCodeMapping = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |]
|
[json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |]
|
||||||
{ matchStatus = 402
|
{ matchStatus = 402
|
||||||
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PT402"]
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PT402"
|
||||||
|
, "Content-Length" <:> "99" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works with sqlstate PGRST custom error" $
|
it "works with sqlstate PGRST custom error" $
|
||||||
@@ -67,5 +72,6 @@ pgErrorCodeMapping = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |]
|
[json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |]
|
||||||
{ matchStatus = 332
|
{ matchStatus = 332
|
||||||
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=123"]
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=123"
|
||||||
|
, "Content-Length" <:> "59" ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ spec actualPgVersion = do
|
|||||||
} |] `shouldRespondWith` ""
|
} |] `shouldRespondWith` ""
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
-- should not have content type set when body is empty
|
-- should not have content type set when body is empty
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, "Content-Length" <:> "0"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "filters columns in result using &select" $
|
it "filters columns in result using &select" $
|
||||||
@@ -42,6 +43,7 @@ spec actualPgVersion = do
|
|||||||
}] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]
|
}] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]
|
||||||
{ matchStatus = 201
|
{ matchStatus = 201
|
||||||
, matchHeaders = [matchContentTypeJson
|
, matchHeaders = [matchContentTypeJson
|
||||||
|
, "Content-Length" <:> "37"
|
||||||
, "Preference-Applied" <:> "return=representation"]
|
, "Preference-Applied" <:> "return=representation"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +189,8 @@ spec actualPgVersion = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType ]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
request methodPost "/auto_incrementing_pk"
|
request methodPost "/auto_incrementing_pk"
|
||||||
@@ -211,7 +214,7 @@ spec actualPgVersion = do
|
|||||||
[json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]
|
[json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]
|
||||||
)
|
)
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "into a table with no pk" $ do
|
context "into a table with no pk" $ do
|
||||||
@@ -286,7 +289,8 @@ spec actualPgVersion = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
|
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ matchContentTypeJson,
|
||||||
|
"Content-Length" <:> "80" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with no payload" $
|
context "with no payload" $
|
||||||
@@ -370,7 +374,8 @@ spec actualPgVersion = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType ]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
request methodPost "/items2"
|
request methodPost "/items2"
|
||||||
@@ -388,7 +393,8 @@ spec actualPgVersion = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType ]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
request methodPost "/items3?select=id"
|
request methodPost "/items3?select=id"
|
||||||
@@ -623,7 +629,8 @@ spec actualPgVersion = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "CSV insert" $ do
|
describe "CSV insert" $ do
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63)
|
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63)
|
||||||
|
|
||||||
@@ -140,6 +141,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 0.06
|
totalCost `shouldBe` 0.06
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 8.23
|
totalCost `shouldBe` 8.23
|
||||||
|
|
||||||
@@ -166,6 +169,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68)
|
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68)
|
||||||
|
|
||||||
@@ -180,6 +184,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 3.55
|
totalCost `shouldBe` 3.55
|
||||||
|
|
||||||
@@ -194,6 +199,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 5.53
|
totalCost `shouldBe` 5.53
|
||||||
|
|
||||||
@@ -248,6 +254,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
aggCol `shouldBe` Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
|
aggCol `shouldBe` Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
|
||||||
|
|
||||||
describe "function plan" $ do
|
describe "function plan" $ do
|
||||||
@@ -261,6 +268,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 68.56
|
totalCost `shouldBe` 68.56
|
||||||
|
|
||||||
@@ -275,6 +283,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
|
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
|
||||||
|
|
||||||
@@ -288,6 +297,7 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
|
||||||
|
resHeaders `shouldSatisfy` notZeroContentLength
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
|
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.private_table'","message":"Could not find the table 'test.faketable' in the schema cache"} |]
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.private_table'","message":"Could not find the table 'test.faketable' in the schema cache"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = []
|
, matchHeaders = ["Content-Length" <:> "166"]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "Filtering response" $ do
|
describe "Filtering response" $ do
|
||||||
it "matches with equality" $
|
it "matches with equality" $
|
||||||
get "/items?id=eq.5"
|
get "/items?id=eq.5"
|
||||||
`shouldRespondWith` [json| [{"id":5}] |]
|
`shouldRespondWith` [json| [{"id":5}] |]
|
||||||
{ matchHeaders = ["Content-Range" <:> "0-0/*"] }
|
{ matchHeaders = [
|
||||||
|
"Content-Range" <:> "0-0/*",
|
||||||
|
"Content-Length" <:> "10"
|
||||||
|
] }
|
||||||
|
|
||||||
it "matches with equality using not operator" $
|
it "matches with equality using not operator" $
|
||||||
get "/items?id=not.eq.5&order=id"
|
get "/items?id=not.eq.5&order=id"
|
||||||
@@ -511,7 +514,10 @@ spec = do
|
|||||||
"code":"PGRST108",
|
"code":"PGRST108",
|
||||||
"message":"'non_existent_projects' is not an embedded resource in this request"}|]
|
"message":"'non_existent_projects' is not an embedded resource in this request"}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "204",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith`
|
get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
@@ -590,7 +596,7 @@ spec = do
|
|||||||
get "/complex_items?select=id::fakecolumntype"
|
get "/complex_items?select=id::fakecolumntype"
|
||||||
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = []
|
, matchHeaders = ["Content-Length" <:> "94"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can cast types with underscore and numbers" $
|
it "can cast types with underscore and numbers" $
|
||||||
@@ -603,7 +609,11 @@ spec = do
|
|||||||
it "requesting parents and children" $
|
it "requesting parents and children" $
|
||||||
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [
|
||||||
|
"Content-Length" <:> "141",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
it "requesting parent and renaming primary key" $
|
it "requesting parent and renaming primary key" $
|
||||||
get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
|
get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
|
||||||
@@ -1145,7 +1155,10 @@ spec = do
|
|||||||
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
|
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
|
||||||
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
|
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "169",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "Accept headers" $ do
|
describe "Accept headers" $ do
|
||||||
@@ -1155,7 +1168,10 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
|
[json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
|
||||||
{ matchStatus = 406
|
{ matchStatus = 406
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "116",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "should respond correctly to */* in accept header" $
|
it "should respond correctly to */* in accept header" $
|
||||||
@@ -1194,7 +1210,10 @@ spec = do
|
|||||||
(acceptHdrs "text/csv; version=1") ""
|
(acceptHdrs "text/csv; version=1") ""
|
||||||
`shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"
|
`shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
, matchHeaders = [
|
||||||
|
"Content-Type" <:> "text/csv; charset=utf-8",
|
||||||
|
"Content-Length" <:> "21"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "Canonical location" $ do
|
describe "Canonical location" $ do
|
||||||
@@ -1512,7 +1531,10 @@ spec = do
|
|||||||
get "/datarep_todos?select=id,label_color,banana" `shouldRespondWith`
|
get "/datarep_todos?select=id,label_color,banana" `shouldRespondWith`
|
||||||
[json| {"code":"42703","details":null,"hint":null,"message":"column datarep_todos.banana does not exist"} |]
|
[json| {"code":"42703","details":null,"hint":null,"message":"column datarep_todos.banana does not exist"} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "98",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
it "formats columns in views including computed columns" $
|
it "formats columns in views including computed columns" $
|
||||||
get "/datarep_todos_computed?select=id,label_color,dark_color" `shouldRespondWith`
|
get "/datarep_todos_computed?select=id,label_color,dark_color" `shouldRespondWith`
|
||||||
@@ -1548,8 +1570,8 @@ spec = do
|
|||||||
get "/datarep_todos?id=lt.4" `shouldRespondWith`
|
get "/datarep_todos?id=lt.4" `shouldRespondWith`
|
||||||
[json| [
|
[json| [
|
||||||
{"id":1,"name":"Report","label_color":"#000000","due_at":"2018-01-02T00:00:00Z","icon_image":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=","created_at":1513213350,"budget":"12.50"},
|
{"id":1,"name":"Report","label_color":"#000000","due_at":"2018-01-02T00:00:00Z","icon_image":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=","created_at":1513213350,"budget":"12.50"},
|
||||||
{"id":2,"name":"Essay","label_color":"#000100","due_at":"2018-01-03T00:00:00Z","icon_image":null,"created_at":1513213350,"budget":"100000000000000.13"},
|
{"id":2,"name":"Essay","label_color":"#000100","due_at":"2018-01-03T00:00:00Z","icon_image":null,"created_at":1513213350,"budget":"100000000000000.13"},
|
||||||
{"id":3,"name":"Algebra","label_color":"#01E240","due_at":"2018-01-01T14:12:34.123456Z","icon_image":null,"created_at":1513213350,"budget":"0.00"}
|
{"id":3,"name":"Algebra","label_color":"#01E240","due_at":"2018-01-01T14:12:34.123456Z","icon_image":null,"created_at":1513213350,"budget":"0.00"}
|
||||||
] |]
|
] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
it "formats star and explicit mix" $
|
it "formats star and explicit mix" $
|
||||||
@@ -1580,7 +1602,10 @@ spec = do
|
|||||||
-- we prove the parser is not used because it'd replace the Z with `+00:00` and a different error message.
|
-- we prove the parser is not used because it'd replace the Z with `+00:00` and a different error message.
|
||||||
[json| {"code":"22007","details":null,"hint":null,"message":"invalid input syntax for type timestamp with time zone: \"Z\""} |]
|
[json| {"code":"22007","details":null,"hint":null,"message":"invalid input syntax for type timestamp with time zone: \"Z\""} |]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "117",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
it "uses text parser for filter with 'IN' predicates" $
|
it "uses text parser for filter with 'IN' predicates" $
|
||||||
get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith`
|
get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith`
|
||||||
@@ -1607,7 +1632,10 @@ spec = do
|
|||||||
{"code":"42883","details":null,"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.","message":"operator does not exist: public.color ~~* unknown"}
|
{"code":"42883","details":null,"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.","message":"operator does not exist: public.color ~~* unknown"}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [
|
||||||
|
"Content-Length" <:> "200",
|
||||||
|
matchContentTypeJson
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "searching for an empty string" $ do
|
context "searching for an empty string" $ do
|
||||||
@@ -1624,11 +1652,13 @@ spec = do
|
|||||||
it "return http status 500" $
|
it "return http status 500" $
|
||||||
get "/infinite_recursion?select=*" `shouldRespondWith`
|
get "/infinite_recursion?select=*" `shouldRespondWith`
|
||||||
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500
|
||||||
|
, matchHeaders = ["Content-Length" <:> "128"] }
|
||||||
|
|
||||||
context "invalid resource path" $ do
|
context "invalid resource path" $ do
|
||||||
it "return http status 404" $
|
it "return http status 404" $
|
||||||
get "/first/second/third?select=*"
|
get "/first/second/third?select=*"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||||
{ matchStatus = 404 }
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = ["Content-Length" <:> "96"]}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ spec = do
|
|||||||
context "when I don't want the count" $ do
|
context "when I don't want the count" $ do
|
||||||
it "returns range Content-Range with */* for empty range" $
|
it "returns range Content-Range with */* for empty range" $
|
||||||
get "/rpc/getitemrange?min=2&max=2"
|
get "/rpc/getitemrange?min=2&max=2"
|
||||||
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
|
`shouldRespondWith` [json| [] |]
|
||||||
|
{ matchHeaders = [ "Content-Range" <:> "*/*"
|
||||||
|
, "Content-Length" <:> "2" ]
|
||||||
|
}
|
||||||
|
|
||||||
it "returns range Content-Range with range/*" $
|
it "returns range Content-Range with range/*" $
|
||||||
get "/rpc/getitemrange?order=id&min=0&max=15"
|
get "/rpc/getitemrange?order=id&min=0&max=15"
|
||||||
@@ -42,7 +45,8 @@ spec = do
|
|||||||
"hint":null
|
"hint":null
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 416
|
{ matchStatus = 416
|
||||||
, matchHeaders = ["Content-Range" <:> "*/0"]
|
, matchHeaders = [ "Content-Range" <:> "*/0"
|
||||||
|
, "Content-Length" <:> "144"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "refuses a range requesting start past last item" $
|
it "refuses a range requesting start past last item" $
|
||||||
@@ -65,8 +69,8 @@ spec = do
|
|||||||
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
|
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
|
||||||
(rangeHdrs $ ByteRangeFromTo 0 1) mempty
|
(rangeHdrs $ ByteRangeFromTo 0 1) mempty
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy` matchHeader "Content-Range" "0-1/*"
|
||||||
matchHeader "Content-Range" "0-1/*"
|
simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "22"
|
||||||
simpleStatus r `shouldBe` ok200
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "understands open-ended ranges" $
|
it "understands open-ended ranges" $
|
||||||
|
|||||||
@@ -31,18 +31,21 @@ spec =
|
|||||||
post "/rpc/getitemrange?limit=1&offset=1" [json| { "min": 2, "max": 4 } |]
|
post "/rpc/getitemrange?limit=1&offset=1" [json| { "min": 2, "max": 4 } |]
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
`shouldRespondWith` [json| [{"id":4}] |]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/*"]
|
, matchHeaders = [ "Content-Range" <:> "1-1/*"
|
||||||
|
, "Content-Length" <:> "10" ]
|
||||||
}
|
}
|
||||||
get "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
|
get "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
`shouldRespondWith` [json| [{"id":4}] |]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/*"]
|
, matchHeaders = [ "Content-Range" <:> "1-1/*"
|
||||||
|
, "Content-Length" <:> "10"]
|
||||||
}
|
}
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1" mempty mempty
|
request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1" mempty mempty
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [ matchContentTypeJson
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "1-1/*" ]
|
, "Content-Range" <:> "1-1/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +213,8 @@ spec =
|
|||||||
"code":"PGRST202",
|
"code":"PGRST202",
|
||||||
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
|
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ "Content-Length" <:> "291"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "should fail with 404 on unknown proc args" $ do
|
it "should fail with 404 on unknown proc args" $ do
|
||||||
@@ -268,7 +272,8 @@ spec =
|
|||||||
"code":"PGRST203",
|
"code":"PGRST203",
|
||||||
"details":null} |]
|
"details":null} |]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ "Content-Length" <:> "353"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works when having uppercase identifiers" $ do
|
it "works when having uppercase identifiers" $ do
|
||||||
@@ -465,7 +470,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "returns null for an integer with null value" $
|
it "returns null for an integer with null value" $
|
||||||
@@ -613,7 +619,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"Cannot use the DELETE method on RPC","code":"PGRST101","details":null,"hint":null}|]
|
[json|{"message":"Cannot use the DELETE method on RPC","code":"PGRST101","details":null,"hint":null}|]
|
||||||
{ matchStatus = 405
|
{ matchStatus = 405
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ "Content-Length" <:> "94"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
it "PATCH fails" $
|
it "PATCH fails" $
|
||||||
request methodPatch "/rpc/sayhello" [] ""
|
request methodPatch "/rpc/sayhello" [] ""
|
||||||
@@ -628,7 +635,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType ]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- now the test
|
-- now the test
|
||||||
@@ -761,7 +769,8 @@ spec =
|
|||||||
get "/rpc/raise_pt402"
|
get "/rpc/raise_pt402"
|
||||||
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|]
|
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|]
|
||||||
{ matchStatus = 402
|
{ matchStatus = 402
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ "Content-Length" <:> "99"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "defaults to status 500 if RAISE code is PT not followed by a number" $
|
it "defaults to status 500 if RAISE code is PT not followed by a number" $
|
||||||
@@ -769,7 +778,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|]
|
[json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ "Content-Length" <:> "61"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "should work with an overloaded function" $ do
|
context "should work with an overloaded function" $ do
|
||||||
@@ -1024,7 +1034,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST111","details":null,"hint":null}|]
|
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST111","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ "Content-Length" <:> "157"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
get "/rpc/bad_guc_headers_2"
|
get "/rpc/bad_guc_headers_2"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
@@ -1051,6 +1062,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
|
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
|
||||||
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]
|
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]
|
||||||
}
|
}
|
||||||
@@ -1113,7 +1125,8 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.status guc must be a valid status code","code":"PGRST112","details":null,"hint":null}|]
|
[json|{"message":"response.status guc must be a valid status code","code":"PGRST112","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ "Content-Length" <:> "106"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "single unnamed param" $ do
|
context "single unnamed param" $ do
|
||||||
@@ -1291,7 +1304,8 @@ spec =
|
|||||||
"details":"line 1: StartTag: invalid element name\n<\n ^"
|
"details":"line 1: StartTag: invalid element name\n<\n ^"
|
||||||
}|]
|
}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [ "Content-Length" <:> "118"
|
||||||
|
, matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- https://github.com/PostgREST/postgrest/issues/1586#issuecomment-696345442
|
-- https://github.com/PostgREST/postgrest/issues/1586#issuecomment-696345442
|
||||||
@@ -1304,7 +1318,10 @@ spec =
|
|||||||
post "/rpc/char_param_insert" [json| { "char_": "abcdefg", "char_arr": "{abc,abcdefg}" } |]
|
post "/rpc/char_param_insert" [json| { "char_": "abcdefg", "char_arr": "{abc,abcdefg}" } |]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"22001","details":null,"hint":null,"message":"value too long for type character(5)"} |]
|
[json| {"code":"22001","details":null,"hint":null,"message":"value too long for type character(5)"} |]
|
||||||
{ matchStatus = 400 }
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [ "Content-Length" <:> "92"
|
||||||
|
, matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
it "modifies the param type from bit to bit varying" $ do
|
it "modifies the param type from bit to bit varying" $ do
|
||||||
get "/rpc/bit_param_select?bit_=101010&bit_arr={101,101010}" `shouldRespondWith`
|
get "/rpc/bit_param_select?bit_=101010&bit_arr={101,101010}" `shouldRespondWith`
|
||||||
@@ -1314,7 +1331,10 @@ spec =
|
|||||||
post "/rpc/bit_param_insert" [json| { "bit_": "101010", "bit_arr": "{101,101010}" } |]
|
post "/rpc/bit_param_insert" [json| { "bit_": "101010", "bit_arr": "{101,101010}" } |]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"22026","details":null,"hint":null,"message":"bit string length 6 does not match type bit(5)"} |]
|
[json| {"code":"22026","details":null,"hint":null,"message":"bit string length 6 does not match type bit(5)"} |]
|
||||||
{ matchStatus = 400 }
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [ "Content-Length" <:> "102"
|
||||||
|
, matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
context "get message and details from raise sqlstate" $ do
|
context "get message and details from raise sqlstate" $ do
|
||||||
it "gets message and details from raise sqlstate PGRST" $ do
|
it "gets message and details from raise sqlstate PGRST" $ do
|
||||||
@@ -1369,7 +1389,10 @@ spec =
|
|||||||
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
|
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
|
||||||
"details":"Invalid JSON value for MESSAGE: 'INVALID'",
|
"details":"Invalid JSON value for MESSAGE: 'INVALID'",
|
||||||
"hint":"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."}|]
|
"hint":"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500
|
||||||
|
, matchHeaders = [ "Content-Length" <:> "263"
|
||||||
|
, matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
it "returns error for invalid JSON in the DETAIL option of the RAISE statement" $
|
it "returns error for invalid JSON in the DETAIL option of the RAISE statement" $
|
||||||
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
|
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
|
||||||
@@ -1395,4 +1418,7 @@ spec =
|
|||||||
it "should return http status 500" $
|
it "should return http status 500" $
|
||||||
request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith`
|
request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith`
|
||||||
[json|{"code":"53400","message":"temporary file size exceeds temp_file_limit (1kB)","details":null,"hint":null}|]
|
[json|{"code":"53400","message":"temporary file size exceeds temp_file_limit (1kB)","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500
|
||||||
|
, matchHeaders = [ "Content-Length" <:> "105"
|
||||||
|
, matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fake' in the schema cache"} |]
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fake' in the schema cache"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = []
|
, matchHeaders = ["Content-Length" <:> "157"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +31,8 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204,
|
{ matchStatus = 204,
|
||||||
matchHeaders = [matchHeaderAbsent hContentType]
|
matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with invalid json payload" $
|
context "with invalid json payload" $
|
||||||
@@ -40,7 +41,8 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
|
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
|
||||||
{ matchStatus = 400,
|
{ matchStatus = 400,
|
||||||
matchHeaders = [matchContentTypeJson]
|
matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Length" <:> "80"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with no payload" $
|
context "with no payload" $
|
||||||
@@ -59,6 +61,7 @@ spec = do
|
|||||||
"\t \n \r { \"id\": 99 } \t \n \r "
|
"\t \n \r { \"id\": 99 } \t \n \r "
|
||||||
`shouldRespondWith` [json|[{"id":99}]|]
|
`shouldRespondWith` [json|[{"id":99}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Length" <:> "11"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "in a nonempty table" $ do
|
context "in a nonempty table" $ do
|
||||||
@@ -69,6 +72,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "0-0/*" ]
|
, "Content-Range" <:> "0-0/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,8 @@ spec = do
|
|||||||
`shouldRespondWith` "[]"
|
`shouldRespondWith` "[]"
|
||||||
{
|
{
|
||||||
matchStatus = 200,
|
matchStatus = 200,
|
||||||
matchHeaders = ["Preference-Applied" <:> "return=representation"]
|
matchHeaders = [ "Preference-Applied" <:> "return=representation"
|
||||||
|
, "Content-Length" <:> "2"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "returns status code 200 when no rows updated" $
|
it "returns status code 200 when no rows updated" $
|
||||||
@@ -107,6 +112,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "0-1/*"
|
, "Content-Range" <:> "0-1/*"
|
||||||
, "Preference-Applied" <:> "tx=commit" ]
|
, "Preference-Applied" <:> "tx=commit" ]
|
||||||
}
|
}
|
||||||
@@ -123,7 +129,8 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can set a column to NULL" $ do
|
it "can set a column to NULL" $ do
|
||||||
@@ -207,6 +214,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "0-0/*"
|
, "Content-Range" <:> "0-0/*"
|
||||||
, "Preference-Applied" <:> "return=minimal"]
|
, "Preference-Applied" <:> "return=minimal"]
|
||||||
}
|
}
|
||||||
@@ -217,6 +225,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "0-0/*"
|
, "Content-Range" <:> "0-0/*"
|
||||||
, "Preference-Applied" <:> "return=minimal"]
|
, "Preference-Applied" <:> "return=minimal"]
|
||||||
}
|
}
|
||||||
@@ -230,6 +239,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +250,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +261,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +273,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +284,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +295,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +365,7 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = []
|
, matchHeaders = ["Content-Length" <:> "158"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "apply defaults on missing values" $ do
|
context "apply defaults on missing values" $ do
|
||||||
@@ -613,6 +628,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType
|
, matchHeaders = [matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Preference-Applied" <:> "return=minimal"]
|
, "Preference-Applied" <:> "return=minimal"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +639,8 @@ spec = do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType]
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Data representations for payload parsing requires Postgres 10 or above.
|
-- Data representations for payload parsing requires Postgres 10 or above.
|
||||||
@@ -636,6 +653,7 @@ spec = do
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "Content-Range" <:> "0-0/*" ]
|
, "Content-Range" <:> "0-0/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ deleteItems =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [matchHeaderAbsent hContentType] }
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength ]
|
||||||
|
}
|
||||||
|
|
||||||
preferDefault = [("Prefer", "return=representation")]
|
preferDefault = [("Prefer", "return=representation")]
|
||||||
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
|
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
|
||||||
@@ -262,4 +264,3 @@ forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do
|
|||||||
preferRollback `shouldRespondToReads` withoutPreferenceApplied
|
preferRollback `shouldRespondToReads` withoutPreferenceApplied
|
||||||
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied
|
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied
|
||||||
preferRollback `shouldRaiseExceptions` withoutPreferenceApplied
|
preferRollback `shouldRaiseExceptions` withoutPreferenceApplied
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "X-Custom-Header" <:> "mykey=myval" ]
|
, "X-Custom-Header" <:> "mykey=myval" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "X-Custom-Header" <:> "mykey=myval" ]
|
, "X-Custom-Header" <:> "mykey=myval" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = [ matchHeaderAbsent hContentType
|
, matchHeaders = [ matchHeaderAbsent hContentType
|
||||||
|
, matchHeaderAbsent hContentLength
|
||||||
, "X-Custom-Header" <:> "mykey=myval" ]
|
, "X-Custom-Header" <:> "mykey=myval" ]
|
||||||
}
|
}
|
||||||
it "can override the Content-Type header" $ do
|
it "can override the Content-Type header" $ do
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ noBlankHeader = notElem mempty
|
|||||||
noProfileHeader :: [Header] -> Bool
|
noProfileHeader :: [Header] -> Bool
|
||||||
noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers
|
noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers
|
||||||
|
|
||||||
|
notZeroContentLength :: [Header] -> Bool
|
||||||
|
notZeroContentLength headers = maybe False (/= "0") $ lookup hContentLength headers
|
||||||
|
|
||||||
authHeader :: BS.ByteString -> BS.ByteString -> Header
|
authHeader :: BS.ByteString -> BS.ByteString -> Header
|
||||||
authHeader typ creds =
|
authHeader typ creds =
|
||||||
(hAuthorization, typ <> " " <> creds)
|
(hAuthorization, typ <> " " <> creds)
|
||||||
|
|||||||
Reference in New Issue
Block a user