diff --git a/postgrest.cabal b/postgrest.cabal index 7394ff7fc..f5a22143d 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -158,13 +158,12 @@ test-suite spec Feature.JsonOperatorSpec Feature.NoJwtSpec Feature.NonexistentSchemaSpec - Feature.PgVersion95Spec - Feature.PgVersion96Spec Feature.ProxySpec Feature.QueryLimitedSpec Feature.QuerySpec Feature.RangeSpec Feature.RootSpec + Feature.RpcPreRequestGucsSpec Feature.RpcSpec Feature.SingularSpec Feature.StructureSpec diff --git a/test/Feature/JsonOperatorSpec.hs b/test/Feature/JsonOperatorSpec.hs index b9318d4bd..9f0d47f3e 100644 --- a/test/Feature/JsonOperatorSpec.hs +++ b/test/Feature/JsonOperatorSpec.hs @@ -7,7 +7,7 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Types (PgVersion, pgVersion112, pgVersion121) +import PostgREST.Types (PgVersion, pgVersion112, pgVersion121, pgVersion95) import Protolude hiding (get) import SpecHelper @@ -207,3 +207,47 @@ spec actualPgVersion = describe "json and jsonb operators" $ do [json| { "data": { "escaped":" \"bar" } } |] `shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |] { matchStatus = 200 , matchHeaders = [] } + + when (actualPgVersion >= pgVersion95) $ + context "json array negative index" $ do + it "can select with negative indexes" $ do + get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith` + [json| [{"data":3}, {"data":6}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith` + [json| [{"data":8}, {"data":7}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith` + [json| [{"a":"A"}, {"a":"[1,2,3]"}] |] + { matchHeaders = [matchContentTypeJson] } + + it "can filter with negative indexes" $ do + get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith` + [json| [{"data":[1, 2, 3]}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith` + [json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith` + [json| [{"data":[{"a": "A"}, {"b": "B"}]}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith` + [json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |] + { matchHeaders = [matchContentTypeJson] } + + it "should fail on badly formed negatives" $ do + get "/json_arr?select=data->>-78xy" `shouldRespondWith` + [json| + {"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input", + "message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |] + { matchStatus = 400, matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->>--34" `shouldRespondWith` + [json| + {"details": "unexpected \"-\" expecting digit", + "message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |] + { matchStatus = 400, matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->>-xy-4" `shouldRespondWith` + [json| + {"details":"unexpected \"x\" expecting digit", + "message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |] + { matchStatus = 400, matchHeaders = [matchContentTypeJson] } diff --git a/test/Feature/PgVersion95Spec.hs b/test/Feature/PgVersion95Spec.hs deleted file mode 100644 index 31f889234..000000000 --- a/test/Feature/PgVersion95Spec.hs +++ /dev/null @@ -1,55 +0,0 @@ -module Feature.PgVersion95Spec where - -import Network.Wai (Application) - -import Test.Hspec -import Test.Hspec.Wai -import Test.Hspec.Wai.JSON - -import Protolude hiding (get) -import SpecHelper - -spec :: SpecWith ((), Application) -spec = describe "features supported on PostgreSQL 9.5" $ - context "json array negative index" $ do - it "can select with negative indexes" $ do - get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith` - [json| [{"data":3}, {"data":6}] |] - { matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith` - [json| [{"data":8}, {"data":7}] |] - { matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith` - [json| [{"a":"A"}, {"a":"[1,2,3]"}] |] - { matchHeaders = [matchContentTypeJson] } - - it "can filter with negative indexes" $ do - get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith` - [json| [{"data":[1, 2, 3]}] |] - { matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith` - [json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |] - { matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith` - [json| [{"data":[{"a": "A"}, {"b": "B"}]}] |] - { matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith` - [json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |] - { matchHeaders = [matchContentTypeJson] } - - it "should fail on badly formed negatives" $ do - get "/json_arr?select=data->>-78xy" `shouldRespondWith` - [json| - {"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input", - "message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |] - { matchStatus = 400, matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data->>--34" `shouldRespondWith` - [json| - {"details": "unexpected \"-\" expecting digit", - "message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |] - { matchStatus = 400, matchHeaders = [matchContentTypeJson] } - get "/json_arr?select=data->>-xy-4" `shouldRespondWith` - [json| - {"details":"unexpected \"x\" expecting digit", - "message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |] - { matchStatus = 400, matchHeaders = [matchContentTypeJson] } diff --git a/test/Feature/PgVersion96Spec.hs b/test/Feature/PgVersion96Spec.hs deleted file mode 100644 index cafac34fe..000000000 --- a/test/Feature/PgVersion96Spec.hs +++ /dev/null @@ -1,216 +0,0 @@ -module Feature.PgVersion96Spec where - -import Network.HTTP.Types -import Network.Wai (Application) -import Network.Wai.Test (SResponse (simpleHeaders)) - -import Test.Hspec -import Test.Hspec.Wai -import Test.Hspec.Wai.JSON - -import Protolude hiding (get) -import SpecHelper - -spec :: SpecWith ((), Application) -spec = - describe "features supported on PostgreSQL 9.6" $ do - context "GUC headers on function calls" $ do - it "succeeds setting the headers" $ do - get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id" - `shouldRespondWith` [json|[{"id": 2}]|] - {matchHeaders = [ - matchContentTypeJson, - "X-Test" <:> "key1=val1; someValue; key2=val2", - "X-Test-2" <:> "key1=val1"]} - get "/rpc/get_int_and_guc_headers?num=1" - `shouldRespondWith` [json|1|] - {matchHeaders = [ - matchContentTypeJson, - "X-Test" <:> "key1=val1; someValue; key2=val2", - "X-Test-2" <:> "key1=val1"]} - post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|] - `shouldRespondWith` [json|1|] - {matchHeaders = [ - matchContentTypeJson, - "X-Test" <:> "key1=val1; someValue; key2=val2", - "X-Test-2" <:> "key1=val1"]} - - it "fails when setting headers with wrong json structure" $ do - get "/rpc/bad_guc_headers_1" - `shouldRespondWith` - [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] - { matchStatus = 500 - , matchHeaders = [ matchContentTypeJson ] - } - get "/rpc/bad_guc_headers_2" - `shouldRespondWith` - [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] - { matchStatus = 500 - , matchHeaders = [ matchContentTypeJson ] - } - get "/rpc/bad_guc_headers_3" - `shouldRespondWith` - [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] - { matchStatus = 500 - , matchHeaders = [ matchContentTypeJson ] - } - post "/rpc/bad_guc_headers_1" [json|{}|] - `shouldRespondWith` - [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] - { matchStatus = 500 - , matchHeaders = [ matchContentTypeJson ] - } - - it "can set the same http header twice" $ - get "/rpc/set_cookie_twice" - `shouldRespondWith` "null" - {matchHeaders = [ - matchContentTypeJson, - "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/", - "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]} - - context "GUC headers on all other methods via pre-request" $ do - it "succeeds setting the headers on GET and HEAD" $ do - request methodGet "/items?id=eq.1" [("User-Agent", "MSIE 6.0")] mempty - `shouldRespondWith` [json|[{"id": 1}]|] - {matchHeaders = [ - matchContentTypeJson, - "Cache-Control" <:> "no-cache, no-store, must-revalidate"]} - - request methodHead "/items?id=eq.1" [("User-Agent", "MSIE 7.0")] mempty - `shouldRespondWith` "" - {matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]} - - request methodHead "/projects" [("Accept", "text/csv")] mempty - `shouldRespondWith` "" - {matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]} - - it "succeeds setting the headers on POST" $ - request methodPost "/items" [] [json|[{"id": 11111}]|] - `shouldRespondWith` "" - { matchStatus = 201 - , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] - } - - it "succeeds setting the headers on PATCH" $ - request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|] - `shouldRespondWith` "" - { matchStatus = 204 - , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] - } - - it "succeeds setting the headers on PUT" $ - request methodPut "/items?id=eq.11111" [] [json|[{"id": 11111}]|] - `shouldRespondWith` "" - { matchStatus = 204 - , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] - } - - it "succeeds setting the headers on DELETE" $ - request methodDelete "/items?id=eq.11111" [] mempty - `shouldRespondWith` "" - { matchStatus = 204 - , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] - } - - context "Override provided headers by using GUC headers" $ do - it "can override the Content-Type header" $ do - request methodHead "/clients?id=eq.1" [] mempty - `shouldRespondWith` "" - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/geo+json"] - } - request methodHead "/rpc/getallprojects" [] mempty - `shouldRespondWith` "" - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/geo+json"] - } - - it "can override the Location header" $ - request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|] - `shouldRespondWith` "" - { matchStatus = 201 - , matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"] - } - - -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535 - -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests. - -- These tests are to ensure no blank headers are added. - context "Blank headers bug" $ do - it "shouldn't add blank headers on POST" $ do - r <- request methodPost "/loc_test" [] [json|{"id": "1", "c": "c1"}|] - liftIO $ do - let respHeaders = simpleHeaders r - respHeaders `shouldSatisfy` noBlankHeader - - it "shouldn't add blank headers on PATCH" $ do - r <- request methodPatch "/loc_test?id=eq.1" [] [json|{"c": "c2"}|] - liftIO $ do - let respHeaders = simpleHeaders r - respHeaders `shouldSatisfy` noBlankHeader - - it "shouldn't add blank headers on GET" $ do - r <- request methodGet "/loc_test" [] "" - liftIO $ do - let respHeaders = simpleHeaders r - respHeaders `shouldSatisfy` noBlankHeader - - it "shouldn't add blank headers on DELETE" $ do - r <- request methodDelete "/loc_test?id=eq.1" [] "" - liftIO $ do - let respHeaders = simpleHeaders r - respHeaders `shouldSatisfy` noBlankHeader - - context "GUC status override" $ do - it "can override the status on RPC" $ - get "/rpc/send_body_status_403" - `shouldRespondWith` - [json|{"message" : "invalid user or password"}|] - { matchStatus = 403 - , matchHeaders = [ matchContentTypeJson ] - } - - it "can override the status through trigger" $ - request methodPatch "/stuff?id=eq.1" [] [json|[{"name": "updated stuff 1"}]|] - `shouldRespondWith` 205 - - it "fails when setting invalid status guc" $ - get "/rpc/send_bad_status" - `shouldRespondWith` - [json|{"message":"response.status guc must be a valid status code"}|] - { matchStatus = 500 - , matchHeaders = [ matchContentTypeJson ] - } - - context "Use of the phraseto_tsquery function" $ do - it "finds matches" $ - get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith` - [json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] - { matchHeaders = [matchContentTypeJson] } - - it "finds matches with different dictionaries" $ - get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith` - [json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |] - { matchHeaders = [matchContentTypeJson] } - - it "can be negated with not operator" $ - get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith` - [json| [ - {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}, - {"text_search_vector": "'also':2 'fun':3 'possibl':8"}, - {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, - {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] - { matchHeaders = [matchContentTypeJson] } - - it "can be used with or query param" $ - get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith` - [json|[ - {"text_search_vector": "'fun':5 'imposs':9 'kind':3" }, - {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, - {"text_search_vector": "'art':4 'spass':5 'unmog':7"} - ]|] { matchHeaders = [matchContentTypeJson] } - - it "should work when used with GET RPC" $ - get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith` - [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] - { matchHeaders = [matchContentTypeJson] } diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 4e3d1260c..12f0d7a24 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -10,7 +10,7 @@ import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Types (PgVersion, pgVersion112, pgVersion121) +import PostgREST.Types (PgVersion, pgVersion96, pgVersion112, pgVersion121) import Protolude hiding (get) import SpecHelper @@ -183,6 +183,35 @@ spec actualPgVersion = do {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] { matchHeaders = [matchContentTypeJson] } + when (actualPgVersion >= pgVersion96) $ + context "Use of the phraseto_tsquery function" $ do + it "finds matches" $ + get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith` + [json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] + { matchHeaders = [matchContentTypeJson] } + + it "finds matches with different dictionaries" $ + get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith` + [json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |] + { matchHeaders = [matchContentTypeJson] } + + it "can be negated with not operator" $ + get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith` + [json| [ + {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}, + {"text_search_vector": "'also':2 'fun':3 'possibl':8"}, + {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, + {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] + { matchHeaders = [matchContentTypeJson] } + + it "can be used with or query param" $ + get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith` + [json|[ + {"text_search_vector": "'fun':5 'imposs':9 'kind':3" }, + {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, + {"text_search_vector": "'art':4 'spass':5 'unmog':7"} + ]|] { matchHeaders = [matchContentTypeJson] } + it "matches with computed column" $ get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |] diff --git a/test/Feature/RpcPreRequestGucsSpec.hs b/test/Feature/RpcPreRequestGucsSpec.hs new file mode 100644 index 000000000..77ff5120b --- /dev/null +++ b/test/Feature/RpcPreRequestGucsSpec.hs @@ -0,0 +1,72 @@ +module Feature.RpcPreRequestGucsSpec where + +import qualified Data.ByteString.Lazy as BL (empty) + +import Network.Wai (Application) +import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus)) + +import Network.HTTP.Types +import Test.Hspec hiding (pendingWith) +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON +import Text.Heredoc + +import Protolude hiding (get) +import SpecHelper + +spec :: SpecWith ((), Application) +spec = + describe "GUC headers on all methods via pre-request" $ do + it "succeeds setting the headers on POST" $ + request methodPost "/items" [] [json|[{"id": 11111}]|] + `shouldRespondWith` "" + { matchStatus = 201 + , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] + } + + it "succeeds setting the headers on GET and HEAD" $ do + request methodGet "/items?id=eq.11111" [("User-Agent", "MSIE 6.0")] mempty + `shouldRespondWith` [json|[{"id": 11111}]|] + {matchHeaders = [ + matchContentTypeJson, + "Cache-Control" <:> "no-cache, no-store, must-revalidate"]} + + request methodHead "/items?id=eq.11111" [("User-Agent", "MSIE 7.0")] mempty + `shouldRespondWith` "" + {matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]} + + request methodHead "/projects" [("Accept", "text/csv")] mempty + `shouldRespondWith` "" + {matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]} + + it "succeeds setting the headers on PATCH" $ + request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|] + `shouldRespondWith` "" + { matchStatus = 204 + , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] + } + + it "succeeds setting the headers on PUT" $ + request methodPut "/items?id=eq.11111" [] [json|[{"id": 11111}]|] + `shouldRespondWith` "" + { matchStatus = 204 + , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] + } + + it "succeeds setting the headers on DELETE" $ + request methodDelete "/items?id=eq.11111" [] mempty + `shouldRespondWith` "" + { matchStatus = 204 + , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] + } + it "can override the Content-Type header" $ do + request methodHead "/clients?id=eq.1" [] mempty + `shouldRespondWith` "" + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/custom+json"] + } + request methodHead "/rpc/getallprojects" [] mempty + `shouldRespondWith` "" + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/custom+json"] + } diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 2251d7813..d04134de3 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -3,7 +3,7 @@ module Feature.RpcSpec where import qualified Data.ByteString.Lazy as BL (empty) import Network.Wai (Application) -import Network.Wai.Test (SResponse (simpleBody, simpleStatus)) +import Network.Wai.Test (SResponse (simpleBody, simpleStatus, simpleHeaders)) import Network.HTTP.Types import Test.Hspec hiding (pendingWith) @@ -12,7 +12,8 @@ import Test.Hspec.Wai.JSON import Text.Heredoc import PostgREST.Types (PgVersion, pgVersion100, pgVersion109, - pgVersion110, pgVersion112, pgVersion114) + pgVersion110, pgVersion112, pgVersion114, + pgVersion96) import Protolude hiding (get) import SpecHelper @@ -787,7 +788,125 @@ spec actualPgVersion = [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] { matchHeaders = [matchContentTypeJson] } + when (actualPgVersion >= pgVersion96) $ + it "should work with the phraseto_tsquery function" $ + get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + it "should work with an argument of custom type in public schema" $ get "/rpc/test_arg?my_arg=something" `shouldRespondWith` [json|"foobar"|] { matchHeaders = [matchContentTypeJson] } + + when (actualPgVersion >= pgVersion96) $ do + context "GUC headers on function calls" $ do + it "succeeds setting the headers" $ do + get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id" + `shouldRespondWith` [json|[{"id": 2}]|] + {matchHeaders = [ + matchContentTypeJson, + "X-Test" <:> "key1=val1; someValue; key2=val2", + "X-Test-2" <:> "key1=val1"]} + get "/rpc/get_int_and_guc_headers?num=1" + `shouldRespondWith` [json|1|] + {matchHeaders = [ + matchContentTypeJson, + "X-Test" <:> "key1=val1; someValue; key2=val2", + "X-Test-2" <:> "key1=val1"]} + post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|] + `shouldRespondWith` [json|1|] + {matchHeaders = [ + matchContentTypeJson, + "X-Test" <:> "key1=val1; someValue; key2=val2", + "X-Test-2" <:> "key1=val1"]} + + it "fails when setting headers with wrong json structure" $ do + get "/rpc/bad_guc_headers_1" + `shouldRespondWith` + [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] + { matchStatus = 500 + , matchHeaders = [ matchContentTypeJson ] + } + get "/rpc/bad_guc_headers_2" + `shouldRespondWith` + [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] + { matchStatus = 500 + , matchHeaders = [ matchContentTypeJson ] + } + get "/rpc/bad_guc_headers_3" + `shouldRespondWith` + [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] + { matchStatus = 500 + , matchHeaders = [ matchContentTypeJson ] + } + post "/rpc/bad_guc_headers_1" [json|{}|] + `shouldRespondWith` + [json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|] + { matchStatus = 500 + , matchHeaders = [ matchContentTypeJson ] + } + + it "can set the same http header twice" $ + get "/rpc/set_cookie_twice" + `shouldRespondWith` "null" + {matchHeaders = [ + matchContentTypeJson, + "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/", + "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]} + + it "can override the Location header on a trigger" $ + request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|] + `shouldRespondWith` "" + { matchStatus = 201 + , matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"] + } + + -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535 + -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests. + -- These tests are to ensure no blank headers are added. + context "Blank headers bug" $ do + it "shouldn't add blank headers on POST" $ do + r <- request methodPost "/loc_test" [] [json|{"id": "1", "c": "c1"}|] + liftIO $ do + let respHeaders = simpleHeaders r + respHeaders `shouldSatisfy` noBlankHeader + + it "shouldn't add blank headers on PATCH" $ do + r <- request methodPatch "/loc_test?id=eq.1" [] [json|{"c": "c2"}|] + liftIO $ do + let respHeaders = simpleHeaders r + respHeaders `shouldSatisfy` noBlankHeader + + it "shouldn't add blank headers on GET" $ do + r <- request methodGet "/loc_test" [] "" + liftIO $ do + let respHeaders = simpleHeaders r + respHeaders `shouldSatisfy` noBlankHeader + + it "shouldn't add blank headers on DELETE" $ do + r <- request methodDelete "/loc_test?id=eq.1" [] "" + liftIO $ do + let respHeaders = simpleHeaders r + respHeaders `shouldSatisfy` noBlankHeader + + context "GUC status override" $ do + it "can override the status on RPC" $ + get "/rpc/send_body_status_403" + `shouldRespondWith` + [json|{"message" : "invalid user or password"}|] + { matchStatus = 403 + , matchHeaders = [ matchContentTypeJson ] + } + + it "can override the status through trigger" $ + request methodPatch "/stuff?id=eq.1" [] [json|[{"name": "updated stuff 1"}]|] + `shouldRespondWith` 205 + + it "fails when setting invalid status guc" $ + get "/rpc/send_bad_status" + `shouldRespondWith` + [json|{"message":"response.status guc must be a valid status code"}|] + { matchStatus = 500 + , matchHeaders = [ matchContentTypeJson ] + } diff --git a/test/Main.hs b/test/Main.hs index 200fd27fc..2d0150745 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -36,14 +36,13 @@ import qualified Feature.JsonOperatorSpec import qualified Feature.MultipleSchemaSpec import qualified Feature.NoJwtSpec import qualified Feature.NonexistentSchemaSpec -import qualified Feature.PgVersion95Spec -import qualified Feature.PgVersion96Spec import qualified Feature.ProxySpec import qualified Feature.QueryLimitedSpec import qualified Feature.QuerySpec import qualified Feature.RangeSpec import qualified Feature.RawOutputTypesSpec import qualified Feature.RootSpec +import qualified Feature.RpcPreRequestGucsSpec import qualified Feature.RpcSpec import qualified Feature.SingularSpec import qualified Feature.StructureSpec @@ -110,7 +109,6 @@ main = do , ("Feature.StructureSpec" , Feature.StructureSpec.spec) , ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion) , ("Feature.UpsertSpec" , Feature.UpsertSpec.spec) - , ("Feature.PgVersion95Spec" , Feature.PgVersion95Spec.spec) ] mutSpecs = uncurry describe <$> [ @@ -173,12 +171,13 @@ main = do before extraSearchPathApp $ describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec - -- this test runs with a root spec function override + when (actualPgVersion >= pgVersion96) $ do + -- this test runs with a root spec function override before rootSpecApp $ describe "Feature.RootSpec" Feature.RootSpec.spec before responseHeadersApp $ - describe "Feature.PgVersion96Spec" Feature.PgVersion96Spec.spec + describe "Feature.RpcPreRequestGucsSpec" Feature.RpcPreRequestGucsSpec.spec -- this test runs with multiple schemas before multipleSchemaApp $ diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index ea1cd48fb..9d848c930 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1699,19 +1699,21 @@ declare user_agent text := current_setting('request.header.user-agent', true); req_path text := current_setting('request.path', true); req_accept text := current_setting('request.header.accept', true); + req_method text := current_setting('request.method', true); begin if user_agent similar to 'MSIE (6.0|7.0)' then perform set_config('response.headers', - '[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false); + '[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', true); elsif req_path similar to '/(items|projects)' and req_accept = 'text/csv' then perform set_config('response.headers', - format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), false); + format('[{"Content-Disposition": "attachment; filename=%s.csv"}]', trim('/' from req_path)), true); elsif req_path similar to '/(clients|rpc/getallprojects)' then perform set_config('response.headers', - '[{"Content-Type": "application/geo+json"}]', false); - else + '[{"Content-Type": "application/custom+json"}]', true); + elsif req_path = '/items' and + req_method similar to 'POST|PATCH|PUT|DELETE' then perform set_config('response.headers', - '[{"X-Custom-Header": "mykey=myval"}]', false); + '[{"X-Custom-Header": "mykey=myval"}]', true); end if; end; $$ language plpgsql;