Allow GET on RPC (#946)
This commit is contained in:
+1
-210
@@ -4,8 +4,7 @@ import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus,simpleBody))
|
||||
import qualified Data.ByteString.Lazy as BL (empty)
|
||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||
|
||||
import SpecHelper
|
||||
import Text.Heredoc
|
||||
@@ -564,214 +563,6 @@ spec = do
|
||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "remote procedure call" $ do
|
||||
context "a proc that returns a set" $ do
|
||||
it "returns paginated results" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
||||
`shouldRespondWith` [json| [{"id":3}] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
it "includes total count if requested" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrsWithCount (ByteRangeFromTo 0 0))
|
||||
[json| { "min": 2, "max": 4 } |]
|
||||
`shouldRespondWith` [json| [{"id":3}] |]
|
||||
{ matchStatus = 206 -- it now knows the response is partial
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||
}
|
||||
|
||||
it "returns proper json" $
|
||||
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
||||
[json| [ {"id": 3}, {"id":4} ] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns CSV" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(acceptHdrs "text/csv")
|
||||
[json| { "min": 2, "max": 4 } |]
|
||||
`shouldRespondWith` "id\n3\n4"
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||
}
|
||||
|
||||
context "unknown function" $
|
||||
it "returns 404" $
|
||||
post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404
|
||||
|
||||
context "shaping the response returned by a proc" $ do
|
||||
it "returns a project" $
|
||||
post "/rpc/getproject" [json| { "id": 1} |] `shouldRespondWith`
|
||||
[str|[{"id":1,"name":"Windows 7","client_id":1}]|]
|
||||
|
||||
it "can filter proc results" $
|
||||
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith`
|
||||
[json|[{"id":2},{"id":3},{"id":4}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can limit proc results" $
|
||||
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
|
||||
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "1-2/*"]
|
||||
}
|
||||
|
||||
it "select works on the first level" $
|
||||
post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`
|
||||
[str|[{"id":1,"name":"Windows 7"}]|]
|
||||
|
||||
context "foreign entities embedding" $ do
|
||||
it "can embed if related tables are in the exposed schema" $
|
||||
post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith`
|
||||
[str|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
|
||||
|
||||
it "cannot embed if the related table is not in the exposed schema" $
|
||||
post "/rpc/single_article?select=*,article_stars{*}" [json|{ "id": 1}|]
|
||||
`shouldRespondWith` 400
|
||||
|
||||
it "can embed if the related tables are in a hidden schema but exposed as views" $
|
||||
post "/rpc/single_article?select=id,articleStars{userId}" [json|{ "id": 2}|]
|
||||
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "a proc that returns an empty rowset" $
|
||||
it "returns empty json array" $
|
||||
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
||||
[json| [] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "proc return types" $ do
|
||||
context "returns text" $ do
|
||||
it "returns proper json" $
|
||||
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
||||
[json|"Hello, world"|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can handle unicode" $
|
||||
post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`
|
||||
[json|"Hello, ¥"|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns array" $
|
||||
post "/rpc/ret_array" [json|{}|] `shouldRespondWith`
|
||||
[json|[1, 2, 3]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns setof integers" $
|
||||
post "/rpc/ret_setof_integers" [json|{}|] `shouldRespondWith`
|
||||
[json|[{ "ret_setof_integers": 1 },
|
||||
{ "ret_setof_integers": 2 },
|
||||
{ "ret_setof_integers": 3 }]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns enum value" $
|
||||
post "/rpc/ret_enum" [json|{ "val": "foo" }|] `shouldRespondWith`
|
||||
[json|"foo"|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns domain value" $
|
||||
post "/rpc/ret_domain" [json|{ "val": "8" }|] `shouldRespondWith`
|
||||
[json|8|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns range" $
|
||||
post "/rpc/ret_range" [json|{ "low": 10, "up": 20 }|] `shouldRespondWith`
|
||||
[json|"[10,20)"|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns row of scalars" $
|
||||
post "/rpc/ret_scalars" [json|{}|] `shouldRespondWith`
|
||||
[json|[{"a":"scalars", "b":"foo", "c":1, "d":"[10,20)"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns composite type in exposed schema" $
|
||||
post "/rpc/ret_point_2d" [json|{}|] `shouldRespondWith`
|
||||
[json|[{"x": 10, "y": 5}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "cannot return composite type in hidden schema" $
|
||||
post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401
|
||||
|
||||
it "returns single row from table" $
|
||||
post "/rpc/single_article?select=id" [json|{"id": 2}|] `shouldRespondWith`
|
||||
[json|[{"id": 2}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns null for void" $
|
||||
post "/rpc/ret_void" [json|{}|] `shouldRespondWith`
|
||||
[json|null|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "improper input" $ do
|
||||
it "rejects unknown content type even if payload is good" $
|
||||
request methodPost "/rpc/sayhello"
|
||||
(acceptHdrs "audio/mpeg3") [json| { "name": "world" } |]
|
||||
`shouldRespondWith` 415
|
||||
it "rejects malformed json payload" $ do
|
||||
p <- request methodPost "/rpc/sayhello"
|
||||
(acceptHdrs "application/json") "sdfsdf"
|
||||
liftIO $ do
|
||||
simpleStatus p `shouldBe` badRequest400
|
||||
isErrorFormat (simpleBody p) `shouldBe` True
|
||||
it "treats simple plpgsql raise as invalid input" $ do
|
||||
p <- post "/rpc/problem" "{}"
|
||||
liftIO $ do
|
||||
simpleStatus p `shouldBe` badRequest400
|
||||
isErrorFormat (simpleBody p) `shouldBe` True
|
||||
|
||||
context "unsupported verbs" $ do
|
||||
it "DELETE fails" $
|
||||
request methodDelete "/rpc/sayhello" [] ""
|
||||
`shouldRespondWith` 405
|
||||
it "PATCH fails" $
|
||||
request methodPatch "/rpc/sayhello" [] ""
|
||||
`shouldRespondWith` 405
|
||||
it "OPTIONS fails" $
|
||||
-- TODO: should return info about the function
|
||||
request methodOptions "/rpc/sayhello" [] ""
|
||||
`shouldRespondWith` 405
|
||||
it "GET fails with 405 on unknown procs" $
|
||||
-- TODO: should this be 404?
|
||||
get "/rpc/fake" `shouldRespondWith` 405
|
||||
it "GET with 405 on known procs" $
|
||||
get "/rpc/sayhello" `shouldRespondWith` 405
|
||||
|
||||
it "executes the proc exactly once per request" $ do
|
||||
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||
[json|1|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||
[json|2|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "expects a single json object" $ do
|
||||
it "does not expand posted json into parameters" $
|
||||
request methodPost "/rpc/singlejsonparam"
|
||||
[("Prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
|
||||
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "accepts parameters from an html form" $
|
||||
request methodPost "/rpc/singlejsonparam"
|
||||
[("Prefer","params=single-object"),("Content-Type", "application/x-www-form-urlencoded")]
|
||||
("integer=7&double=2.71828&varchar=forms+are+fun&" <>
|
||||
"boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith`
|
||||
[json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun"
|
||||
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "a proc that receives no parameters" $
|
||||
it "interprets empty string as empty json object on a post request" $
|
||||
post "/rpc/noparamsproc" BL.empty `shouldRespondWith`
|
||||
[json| "Return value of no parameters procedure." |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "returns proper output when having the same return col name as the proc name" $
|
||||
post "/rpc/test" [json|{}|] `shouldRespondWith`
|
||||
[json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "weird requests" $ do
|
||||
it "can query as normal" $ do
|
||||
get "/Escap3e;" `shouldRespondWith`
|
||||
|
||||
Reference in New Issue
Block a user