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 "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] }