Update hspec-wai to 0.8.0 (#799)

This commit is contained in:
Diogo Biazus
2017-02-14 21:18:01 -08:00
committed by Joe Nelson
parent 98438c437f
commit 728e40c96c
14 changed files with 229 additions and 290 deletions
+1 -1
View File
@@ -143,7 +143,7 @@ Test-Suite spec
, hjsonpointer , hjsonpointer
, hjsonschema , hjsonschema
, hspec , hspec
, hspec-wai , hspec-wai >= 0.7.0
, hspec-wai-json , hspec-wai-json
, http-types , http-types
, lens , lens
+3 -4
View File
@@ -40,9 +40,8 @@ import Options.Applicative hiding (str)
import Paths_postgrest (version) import Paths_postgrest (version)
import Text.Heredoc import Text.Heredoc
import Text.PrettyPrint.ANSI.Leijen hiding ((<>), (<$>)) import Text.PrettyPrint.ANSI.Leijen hiding ((<>), (<$>))
import qualified Text.PrettyPrint.ANSI.Leijen as L
import Protolude hiding (intercalate import Protolude hiding (intercalate, (<>))
, (<>))
-- | Config file settings for the server -- | Config file settings for the server
data AppConfig = AppConfig { data AppConfig = AppConfig {
@@ -129,7 +128,7 @@ readOptions = do
) )
<> footerDoc (Just $ <> footerDoc (Just $
text "Example Config File:" text "Example Config File:"
<> nest 2 (hardline <> exampleCfg) L.<> nest 2 (hardline L.<> exampleCfg)
) )
parserPrefs = prefs showHelpOnError parserPrefs = prefs showHelpOnError
+1 -1
View File
@@ -39,7 +39,7 @@ data Table = Table {
, tableInsertable :: Bool , tableInsertable :: Bool
} deriving (Show, Ord) } deriving (Show, Ord)
data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord) newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column = data Column =
Column { Column {
+1 -3
View File
@@ -1,9 +1,7 @@
resolver: lts-7.4 resolver: lts-8.0
extra-deps: extra-deps:
- Ranged-sets-0.3.0 - Ranged-sets-0.3.0
- hasql-pool-0.4.1 - hasql-pool-0.4.1
- hasql-transaction-0.5
- optparse-applicative-0.13.0.0
ghc-options: ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
nix: nix:
+20 -29
View File
@@ -18,44 +18,40 @@ spec = describe "authorization" $ do
let single = ("Accept","application/vnd.pgrst.object+json") let single = ("Accept","application/vnd.pgrst.object+json")
it "denies access to tables that anonymous does not own" $ it "denies access to tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` ResponseMatcher { get "/authors_only" `shouldRespondWith` [json| {
matchBody = Just [json| {
"hint":null, "hint":null,
"details":null, "details":null,
"code":"42501", "code":"42501",
"message":"permission denied for relation authors_only"} |] "message":"permission denied for relation authors_only"} |]
, matchStatus = 401 { matchStatus = 401
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"] , matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
} }
it "denies access to tables that postgrest_test_author does not own" $ it "denies access to tables that postgrest_test_author does not own" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" in
request methodGet "/private_table" [auth] "" request methodGet "/private_table" [auth] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {
matchBody = Just [json| {
"hint":null, "hint":null,
"details":null, "details":null,
"code":"42501", "code":"42501",
"message":"permission denied for relation private_table"} |] "message":"permission denied for relation private_table"} |]
, matchStatus = 403 { matchStatus = 403
, matchHeaders = [] , matchHeaders = []
} }
it "returns jwt functions as jwt tokens" $ it "returns jwt functions as jwt tokens" $
request methodPost "/rpc/login" [single] request methodPost "/rpc/login" [single]
[json| { "id": "jdoe", "pass": "1234" } |] [json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |]
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.P2G9EVSVI22MWxXWFuhEYd9BZerLS1WDlqzdqplM15s"} |] { matchStatus = 200
, matchStatus = 200 , matchHeaders = [matchContentTypeJson]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
} }
it "sql functions can encode custom and standard claims" $ it "sql functions can encode custom and standard claims" $
request methodPost "/rpc/jwt_test" [single] "{}" request methodPost "/rpc/jwt_test" [single] "{}"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |]
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.IHF16ZSU6XTbOnUWO8CCpUn2fJwt8P00rlYVyXQjpWc"} |] { matchStatus = 200
, matchStatus = 200 , matchHeaders = [matchContentTypeJson]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
} }
it "sql functions can read custom and standard claims variables" $ do it "sql functions can read custom and standard claims variables" $ do
@@ -82,9 +78,8 @@ spec = describe "authorization" $ do
it "fails with an expired token" $ do it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"message":"JWT expired"} |]
matchBody = Nothing { matchStatus = 401
, matchStatus = 401
, matchHeaders = [ , matchHeaders = [
"WWW-Authenticate" <:> "WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"JWT expired\"" "Bearer error=\"invalid_token\", error_description=\"JWT expired\""
@@ -94,9 +89,8 @@ spec = describe "authorization" $ do
it "hides tables from users with invalid JWT" $ do it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"message":"JWT invalid"} |]
matchBody = Nothing { matchStatus = 401
, matchStatus = 401
, matchHeaders = [ , matchHeaders = [
"WWW-Authenticate" <:> "WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"JWT invalid\"" "Bearer error=\"invalid_token\", error_description=\"JWT invalid\""
@@ -126,9 +120,8 @@ spec = describe "authorization" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.mI2HNoOum6xM3sc4oHLxU4yLv-_WV5W1kqBfY_wEvLw" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.mI2HNoOum6xM3sc4oHLxU4yLv-_WV5W1kqBfY_wEvLw" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|"postgrest_test_author"|]
matchBody = Just [str|"postgrest_test_author"|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
@@ -136,9 +129,8 @@ spec = describe "authorization" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.W7jLsG-zswM91AJkCvZeIMHrnz7_6ceY2jnscVl3Yhk" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.W7jLsG-zswM91AJkCvZeIMHrnz7_6ceY2jnscVl3Yhk" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|"postgrest_test_default_role"|]
matchBody = Just [str|"postgrest_test_default_role"|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
@@ -146,8 +138,7 @@ spec = describe "authorization" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.15Gy8PezQhJIaHYDJVLa-Gmz9T3sJnW66EKAYIsXc7c" in let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.15Gy8PezQhJIaHYDJVLa-Gmz9T3sJnW66EKAYIsXc7c" in
request methodPost "/rpc/get_current_user" [auth] request methodPost "/rpc/get_current_user" [auth]
[json| {} |] [json| {} |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
matchBody = Just [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|] { matchStatus = 400
, matchStatus = 400
, matchHeaders = [] , matchHeaders = []
} }
+2 -3
View File
@@ -24,14 +24,13 @@ spec =
it "should not raise 'transaction in progress' error" $ it "should not raise 'transaction in progress' error" $
raceTest 10 $ raceTest 10 $
get "/fakefake" get "/fakefake"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json|
matchBody = Just [json|
{ "hint": null, { "hint": null,
"details":null, "details":null,
"code":"42P01", "code":"42P01",
"message":"relation \"test.fakefake\" does not exist" "message":"relation \"test.fakefake\" does not exist"
} |] } |]
, matchStatus = 404 { matchStatus = 404
, matchHeaders = [] , matchHeaders = []
} }
+12 -18
View File
@@ -15,24 +15,21 @@ spec =
context "existing record" $ do context "existing record" $ do
it "succeeds with 204 and deletion count" $ it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] "" request methodDelete "/items?id=eq.1" [] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ""
matchBody = Nothing { matchStatus = 204
, matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "returns the deleted item and count if requested" $ it "returns the deleted item and count if requested" $
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] "" request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":2}]|]
matchBody = Just [str|[{"id":2}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/1"] , matchHeaders = ["Content-Range" <:> "*/1"]
} }
it "returns the deleted item and shapes the response" $ it "returns the deleted item and shapes the response" $
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` ResponseMatcher { `shouldRespondWith` [str|[{"id":2,"name":"Two"}]|]
matchBody = Just [str|[{"id":2,"name":"Two"}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "can rename and cast the selected columns" $ it "can rename and cast the selected columns" $
@@ -40,18 +37,16 @@ spec =
`shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|] `shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|]
it "can embed (parent) entities" $ it "can embed (parent) entities" $
request methodDelete "/tasks?id=eq.8&select=id,name,project{id}" [("Prefer", "return=representation")] "" request methodDelete "/tasks?id=eq.8&select=id,name,project{id}" [("Prefer", "return=representation")] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
matchBody = Just [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "actually clears items ouf the db" $ do it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] "" _ <- request methodDelete "/items?id=lt.15" [] ""
get "/items" get "/items"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":15}]|]
matchBody = Just [str|[{"id":15}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
@@ -59,9 +54,8 @@ spec =
it "includes [] body if return=rep" $ it "includes [] body if return=rep" $
request methodDelete "/items?id=eq.101" request methodDelete "/items?id=eq.101"
[("Prefer", "return=representation")] "" [("Prefer", "return=representation")] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]" { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
+48 -61
View File
@@ -3,6 +3,7 @@ module Feature.InsertSpec where
import Test.Hspec hiding (pendingWith) import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Test.Hspec.Wai.Matcher (bodyEquals)
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus)) import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
import SpecHelper import SpecHelper
@@ -43,31 +44,29 @@ spec = do
"integer": 14, "double": 3.14159, "varchar": "testing!" "integer": 14, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99" , "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo" , "enum": "foo"
}] |] `shouldRespondWith` ResponseMatcher { }] |] `shouldRespondWith` [str|[{"integer":14,"varchar":"testing!"}]|]
matchBody = Just [str|[{"integer":14,"varchar":"testing!"}]|] { matchStatus = 201
, matchStatus = 201 , matchHeaders = [matchContentTypeJson]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
} }
context "requesting full representation" $ do context "requesting full representation" $ do
it "includes related data after insert" $ it "includes related data after insert" $
request methodPost "/projects?select=id,name,clients{id,name}" request methodPost "/projects?select=id,name,clients{id,name}"
[("Prefer", "return=representation"), ("Prefer", "count=exact")] [("Prefer", "return=representation"), ("Prefer", "count=exact")]
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher { [str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` [str|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|]
matchBody = Just [str|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|] { matchStatus = 201
, matchStatus = 201 , matchHeaders = [ matchContentTypeJson
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
, "Location" <:> "/projects?id=eq.6" , "Location" <:> "/projects?id=eq.6"
, "Content-Range" <:> "*/1" ] , "Content-Range" <:> "*/1" ]
} }
it "can rename and cast the selected columns" $ it "can rename and cast the selected columns" $
request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text" request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[str|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher { [str|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith`
matchBody = Just [str|[{"pId":"7","pName":"New Project","cId":"2"}]|] [str|[{"pId":"7","pName":"New Project","cId":"2"}]|]
, matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8" , matchHeaders = [ matchContentTypeJson
, "Location" <:> "/projects?id=eq.7" , "Location" <:> "/projects?id=eq.7"
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -201,9 +200,8 @@ spec = do
request methodPost "/json" request methodPost "/json"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
inserted inserted
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"data":{"foo":"bar"}}]|]
matchBody = Just [str|[{"data":{"foo":"bar"}}]|] { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Location" <:> location] , matchHeaders = ["Location" <:> location]
} }
@@ -213,39 +211,35 @@ spec = do
request methodPost "/json" request methodPost "/json"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
inserted inserted
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"data":[1,2,3]}]|]
matchBody = Just [str|[{"data":[1,2,3]}]|] { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Location" <:> location] , matchHeaders = ["Location" <:> location]
} }
context "empty object" $ context "empty object" $
it "successfully populates table with all-default columns" $ it "successfully populates table with all-default columns" $
post "/items" "{}" `shouldRespondWith` ResponseMatcher { post "/items" "{}" `shouldRespondWith` ""
matchBody = Just "" { matchStatus = 201
, matchStatus = 201
, matchHeaders = [] , matchHeaders = []
} }
context "table with limited privileges" $ do context "table with limited privileges" $ do
it "succeeds if correct select is applied" $ it "succeeds if correct select is applied" $
request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")] request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")]
[json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` ResponseMatcher { [json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [str|[{"article_id":2,"user_id":1}]|]
matchBody = Just [str|[{"article_id":2,"user_id":1}]|] { matchStatus = 201
, matchStatus = 201
, matchHeaders = [] , matchHeaders = []
} }
it "fails if more columns are selected" $ it "fails if more columns are selected" $
request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")] request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")]
[json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` ResponseMatcher { [json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith`
matchBody = Just [str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|] [str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
, matchStatus = 401 { matchStatus = 401
, matchHeaders = [] , matchHeaders = []
} }
it "fails if select is not specified" $ it "fails if select is not specified" $
request methodPost "/limited_article_stars" [("Prefer", "return=representation")] request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` ResponseMatcher { [json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` [str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
matchBody = Just [str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|] { matchStatus = 401
, matchStatus = 401
, matchHeaders = [] , matchHeaders = []
} }
@@ -259,11 +253,10 @@ spec = do
|12,0.1,a string,true,1929-10-01,12,bar |12,0.1,a string,true,1929-10-01,12,bar
|] |]
request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted
`shouldRespondWith` ResponseMatcher
`shouldRespondWith` ResponseMatcher { { matchStatus = 201
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
, matchBody = bodyEquals inserted
} }
context "requesting full representation" $ do context "requesting full representation" $ do
@@ -271,9 +264,8 @@ spec = do
request methodPost "/no_pk" request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz" "a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "a,b\nbar,baz"
matchBody = Just "a,b\nbar,baz" { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8", , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"] "Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
} }
@@ -282,9 +274,8 @@ spec = do
request methodPost "/no_pk" request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo" "a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "a,b\n,foo"
matchBody = Just "a,b\n,foo" { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8", , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"] "Location" <:> "/no_pk?a=is.null&b=eq.foo"]
} }
@@ -293,9 +284,8 @@ spec = do
request methodPost "/projects?select=id" request methodPost "/projects?select=id"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"id,name,client_id\n8,Xenix,1\n9,Windows NT,1" "id,name,client_id\n8,Xenix,1\n9,Windows NT,1"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "id\n8\n9"
matchBody = Just "id\n8\n9" { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8", , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Content-Range" <:> "*/*"] "Content-Range" <:> "*/*"]
} }
@@ -334,9 +324,8 @@ spec = do
it "indicates no records found to update" $ it "indicates no records found to update" $
request methodPatch "/empty_table" [] request methodPatch "/empty_table" []
[json| { "extra":20 } |] [json| { "extra":20 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ""
matchBody = Just "", { matchStatus = 204,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -346,9 +335,8 @@ spec = do
liftIO $ simpleHeaders g liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*" `shouldSatisfy` matchHeader "Content-Range" "\\*/\\*"
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |] p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
pure p `shouldRespondWith` ResponseMatcher { pure p `shouldRespondWith` ""
matchBody = Nothing, { matchStatus = 204,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/*"] matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
liftIO $ lookup hContentType (simpleHeaders p) `shouldBe` Nothing liftIO $ lookup hContentType (simpleHeaders p) `shouldBe` Nothing
@@ -363,8 +351,8 @@ spec = do
it "returns empty array when no rows updated and return=rep" $ it "returns empty array when no rows updated and return=rep" $
request methodPatch "/items?id=eq.999999" request methodPatch "/items?id=eq.999999"
[("Prefer", "return=representation")] [json| { "id":999999 } |] [("Prefer", "return=representation")] [json| { "id":999999 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]", {
matchStatus = 200, matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -372,9 +360,8 @@ spec = do
it "returns updated object as array when return=rep" $ it "returns updated object as array when return=rep" $
request methodPatch "/items?id=eq.2" request methodPatch "/items?id=eq.2"
[("Prefer", "return=representation")] [json| { "id":2 } |] [("Prefer", "return=representation")] [json| { "id":2 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":2}]|]
matchBody = Just [str|[{"id":2}]|], { matchStatus = 200,
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "0-0/*"] matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
@@ -395,15 +382,15 @@ spec = do
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |] _ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |]
get "/no_pk?a=eq.keepme" `shouldRespondWith` get "/no_pk?a=eq.keepme" `shouldRespondWith`
[json| [{ a: "keepme", b: null }] |] [json| [{ a: "keepme", b: null }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can set a json column to escaped value" $ do it "can set a json column to escaped value" $ do
_ <- post "/json" [json| { data: {"escaped":"bar"} } |] _ <- post "/json" [json| { data: {"escaped":"bar"} } |]
request methodPatch "/json?data->>escaped=eq.bar" request methodPatch "/json?data->>escaped=eq.bar"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| { "data": { "escaped":" \"bar" } } |] [json| { "data": { "escaped":" \"bar" } } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
matchBody = Just [json| [{ "data": { "escaped":" \"bar" } }] |] { matchStatus = 200
, matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
@@ -412,9 +399,8 @@ spec = do
"/items?always_true=eq.false" "/items?always_true=eq.false"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| { id: 100 } |] [json| { id: 100 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]", { matchStatus = 200,
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -426,6 +412,7 @@ spec = do
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| { id: 99 } |] [json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |] `shouldRespondWith` [json| [{id:99}] |]
{ matchHeaders = [matchContentTypeJson] }
context "with unicode values" $ context "with unicode values" $
it "succeeds and returns values intact" $ do it "succeeds and returns values intact" $ do
+6 -10
View File
@@ -16,9 +16,8 @@ spec =
describe "Requesting many items with server limits enabled" $ do describe "Requesting many items with server limits enabled" $ do
it "restricts results" $ it "restricts results" $
get "/items" get "/items"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":2}] |]
matchBody = Just [json| [{"id":1},{"id":2}] |] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
@@ -32,17 +31,14 @@ spec =
it "limit works on all levels" $ it "limit works on all levels" $
get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc" get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
matchBody = Just [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "limit is not applied to parent embeds" $ it "limit is not applied to parent embeds" $
get "/tasks?select=id,project{id}&id=gt.5" get "/tasks?select=id,project{id}&id=gt.5"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
matchBody = Just [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
+87 -90
View File
@@ -30,68 +30,49 @@ spec = do
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` ResponseMatcher { `shouldRespondWith` [json| [{"id":5}] |]
matchBody = Just [json| [{"id":5}] |] { matchHeaders = ["Content-Range" <:> "0-0/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
it "matches with equality using not operator" $ it "matches with equality using not operator" $
get "/items?id=not.eq.5" get "/items?id=not.eq.5"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |] { matchHeaders = ["Content-Range" <:> "0-13/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-13/*"]
}
it "matches with more than one condition using not operator" $ it "matches with more than one condition using not operator" $
get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]" get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"
it "matches with inequality using not operator" $ do it "matches with inequality using not operator" $ do
get "/items?id=not.lt.14&order=id.asc" get "/items?id=not.lt.14&order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":14},{"id":15}] |]
matchBody = Just [json| [{"id":14},{"id":15}] |] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
get "/items?id=not.gt.2&order=id.asc" get "/items?id=not.gt.2&order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":2}] |]
matchBody = Just [json| [{"id":1},{"id":2}] |] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
it "matches items IN" $ it "matches items IN" $
get "/items?id=in.1,3,5" get "/items?id=in.1,3,5"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |] { matchHeaders = ["Content-Range" <:> "0-2/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"]
}
it "matches items NOT IN" $ it "matches items NOT IN" $
get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15" get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |] { matchHeaders = ["Content-Range" <:> "0-2/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"]
}
it "matches items NOT IN using not operator" $ it "matches items NOT IN using not operator" $
get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15" get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":3},{"id":5}] |]
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |] { matchHeaders = ["Content-Range" <:> "0-2/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"]
}
it "matches nulls using not operator" $ it "matches nulls using not operator" $
get "/no_pk?a=not.is.null" `shouldRespondWith` get "/no_pk?a=not.is.null" `shouldRespondWith`
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |] [json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches nulls in varchar and numeric fields alike" $ do it "matches nulls in varchar and numeric fields alike" $ do
get "/no_pk?a=is.null" `shouldRespondWith` get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |] [json| [{"a": null, "b": null}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/nullable_integer?a=is.null" `shouldRespondWith` [str|[{"a":null}]|] get "/nullable_integer?a=is.null" `shouldRespondWith` [str|[{"a":null}]|]
@@ -119,25 +100,28 @@ spec = do
it "matches with tsearch @@" $ it "matches with tsearch @@" $
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith` get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'bar':2 'foo':1"}] |] [json| [{"text_search_vector":"'bar':2 'foo':1"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches with tsearch @@ using not operator" $ it "matches with tsearch @@ using not operator" $
get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith` get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'baz':1 'qux':2"}] |] [json| [{"text_search_vector":"'baz':1 'qux':2"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches with computed column" $ it "matches with computed column" $
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith` 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}] |] [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}] |]
{ matchHeaders = [matchContentTypeJson] }
it "order by computed column" $ it "order by computed column" $
get "/items?order=anti_id.desc" `shouldRespondWith` get "/items?order=anti_id.desc" `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}] |] [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}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches filtering nested items 2" $ it "matches filtering nested items 2" $
get "/clients?select=id,projects{id,tasks2{id,name}}&projects.tasks.name=like.Design*" get "/clients?select=id,projects{id,tasks2{id,name}}&projects.tasks.name=like.Design*"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"message":"could not find foreign keys between these entities, no relation between projects and tasks2"}|]
matchBody = Just [json| {"message":"could not find foreign keys between these entities, no relation between projects and tasks2"}|] { matchStatus = 400
, matchStatus = 400 , matchHeaders = [matchContentTypeJson]
, matchHeaders = []
} }
it "matches filtering nested items" $ it "matches filtering nested items" $
@@ -166,45 +150,50 @@ spec = do
it "one simple column" $ it "one simple column" $
get "/complex_items?select=id" `shouldRespondWith` get "/complex_items?select=id" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3}] |] [json| [{"id":1},{"id":2},{"id":3}] |]
{ matchHeaders = [matchContentTypeJson] }
it "rename simple column" $ it "rename simple column" $
get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith` get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith`
[json| [{"myId":1}] |] [json| [{"myId":1}] |]
{ matchHeaders = [matchContentTypeJson] }
it "one simple column with casting (text)" $ it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith` get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |] [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "rename simple column with casting" $ it "rename simple column with casting" $
get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith` get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith`
[json| [{"myId":"1"}] |] [json| [{"myId":"1"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "json column" $ it "json column" $
get "/complex_items?id=eq.1&select=settings" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |] [json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "json subfield one level with casting (json)" $ it "json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text" [json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield one level with casting (json)" $ it "rename json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith` get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text" [json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "fails on bad casting (data of the wrong format)" $ it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer" get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
matchBody = Just [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |] { matchStatus = 400
, matchStatus = 400
, matchHeaders = [] , matchHeaders = []
} }
it "fails on bad casting (wrong cast type)" $ it "fails on bad casting (wrong cast type)" $
get "/complex_items?select=id::fakecolumntype" get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
matchBody = Just [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |] { matchStatus = 400
, matchStatus = 400
, matchHeaders = [] , matchHeaders = []
} }
@@ -212,19 +201,23 @@ spec = do
it "json subfield two levels (string)" $ it "json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |] [json| [{"bar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield two levels (string)" $ it "rename json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith` get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
[json| [{"myBar":"baz"}] |] [json| [{"myBar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "json subfield two levels with casting (int)" $ it "json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith` get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now [json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
it "rename json subfield two levels with casting (int)" $ it "rename json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith` get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now [json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
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`
@@ -309,67 +302,63 @@ spec = do
describe "ordering response" $ do describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":2}] |]
matchBody = Just [json| [{"id":1},{"id":2}] |] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "by a column desc" $ it "by a column desc" $
get "/items?id=lte.2&order=id.desc" get "/items?id=lte.2&order=id.desc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":2},{"id":1}] |]
matchBody = Just [json| [{"id":2},{"id":1}] |] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "by a column with nulls first" $ it "by a column with nulls first" $
get "/no_pk?order=a.nullsfirst" get "/no_pk?order=a.nullsfirst"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"a":null,"b":null},
matchBody = Just [json| [{"a":null,"b":null},
{"a":"1","b":"0"}, {"a":"1","b":"0"},
{"a":"2","b":"0"} {"a":"2","b":"0"}
] |] ] |]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "by a column asc with nulls last" $ it "by a column asc with nulls last" $
get "/no_pk?order=a.asc.nullslast" get "/no_pk?order=a.asc.nullslast"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"a":"1","b":"0"},
matchBody = Just [json| [{"a":"1","b":"0"},
{"a":"2","b":"0"}, {"a":"2","b":"0"},
{"a":null,"b":null}] |] {"a":null,"b":null}] |]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "by a column desc with nulls first" $ it "by a column desc with nulls first" $
get "/no_pk?order=a.desc.nullsfirst" get "/no_pk?order=a.desc.nullsfirst"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"a":null,"b":null},
matchBody = Just [json| [{"a":null,"b":null},
{"a":"2","b":"0"}, {"a":"2","b":"0"},
{"a":"1","b":"0"}] |] {"a":"1","b":"0"}] |]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "by a column desc with nulls last" $ it "by a column desc with nulls last" $
get "/no_pk?order=a.desc.nullslast" get "/no_pk?order=a.desc.nullslast"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"a":"2","b":"0"},
matchBody = Just [json| [{"a":"2","b":"0"},
{"a":"1","b":"0"}, {"a":"1","b":"0"},
{"a":null,"b":null}] |] {"a":null,"b":null}] |]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "by a json column property asc" $ it "by a json column property asc" $
get "/json?order=data->>id.asc" `shouldRespondWith` get "/json?order=data->>id.asc" `shouldRespondWith`
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |] [json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "by a json column with two level property nulls first" $ it "by a json column with two level property nulls first" $
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith` get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |] [json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "without other constraints" $ it "without other constraints" $
get "/items?order=id.asc" `shouldRespondWith` 200 get "/items?order=id.asc" `shouldRespondWith` 200
@@ -436,18 +425,16 @@ spec = do
it "should respond with CSV to 'text/csv' request" $ it "should respond with CSV to 'text/csv' request" $
request methodGet "/simple_pk" request methodGet "/simple_pk"
(acceptHdrs "text/csv; version=1") "" (acceptHdrs "text/csv; version=1") ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"
matchBody = Just "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"]
} }
describe "Canonical location" $ do describe "Canonical location" $ do
it "Sets Content-Location with alphabetized params" $ it "Sets Content-Location with alphabetized params" $
get "/no_pk?b=eq.1&a=eq.1" get "/no_pk?b=eq.1&a=eq.1"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]" { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"] , matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
} }
@@ -462,23 +449,26 @@ spec = do
it "can filter by properties inside json column" $ do it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith` get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |] [json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith` get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |] [json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using not" $ it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith` get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |] [json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using ->>" $ it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith` get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |] [json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
describe "remote procedure call" $ do describe "remote procedure call" $ do
context "a proc that returns a set" $ do context "a proc that returns a set" $ do
it "returns paginated results" $ it "returns paginated results" $
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |] (rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":3}] |]
matchBody = Just [json| [{"id":3}] |] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
@@ -486,9 +476,8 @@ spec = do
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) (rangeHdrsWithCount (ByteRangeFromTo 0 0))
[json| { "min": 2, "max": 4 } |] [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":3}] |]
matchBody = Just [json| [{"id":3}] |] { matchStatus = 206 -- it now knows the response is partial
, matchStatus = 206 -- it now knows the response is partial
, matchHeaders = ["Content-Range" <:> "0-0/2"] , matchHeaders = ["Content-Range" <:> "0-0/2"]
} }
@@ -496,6 +485,7 @@ spec = do
it "returns proper json" $ it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith` post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |] [json| [ {"id": 3}, {"id":4} ] |]
{ matchHeaders = [matchContentTypeJson] }
context "unknown function" $ context "unknown function" $
it "returns 404" $ it "returns 404" $
@@ -509,12 +499,12 @@ spec = do
it "can filter proc results" $ it "can filter proc results" $
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith` post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith`
[json|[{"id":2},{"id":3},{"id":4}]|] [json|[{"id":2},{"id":3},{"id":4}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can limit proc results" $ it "can limit proc results" $
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |] post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json|[{"id":3},{"id":4}]|]
matchBody = Just [json|[{"id":3},{"id":4}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "1-2/*"] , matchHeaders = ["Content-Range" <:> "1-2/*"]
} }
@@ -530,15 +520,18 @@ spec = do
it "returns empty json array" $ it "returns empty json array" $
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith` post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
[json| [] |] [json| [] |]
{ matchHeaders = [matchContentTypeJson] }
context "a proc that returns plain text" $ do context "a proc that returns plain text" $ do
it "returns proper json" $ it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith` post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json|"Hello, world"|] [json|"Hello, world"|]
{ matchHeaders = [matchContentTypeJson] }
it "can handle unicode" $ it "can handle unicode" $
post "/rpc/sayhello" [json| { "name": "" } |] `shouldRespondWith` post "/rpc/sayhello" [json| { "name": "" } |] `shouldRespondWith`
[json|"Hello, ¥"|] [json|"Hello, ¥"|]
{ matchHeaders = [matchContentTypeJson] }
context "improper input" $ do context "improper input" $ do
it "rejects unknown content type even if payload is good" $ it "rejects unknown content type even if payload is good" $
@@ -577,14 +570,17 @@ spec = do
it "executes the proc exactly once per request" $ do it "executes the proc exactly once per request" $ do
post "/rpc/callcounter" [json| {} |] `shouldRespondWith` post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json|1|] [json|1|]
{ matchHeaders = [matchContentTypeJson] }
post "/rpc/callcounter" [json| {} |] `shouldRespondWith` post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json|2|] [json|2|]
{ matchHeaders = [matchContentTypeJson] }
context "expects a single json object" $ do context "expects a single json object" $ do
it "does not expand posted json into parameters" $ it "does not expand posted json into parameters" $
request methodPost "/rpc/singlejsonparam" request methodPost "/rpc/singlejsonparam"
[("Prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith` [("Prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
{ matchHeaders = [matchContentTypeJson] }
it "accepts parameters from an html form" $ it "accepts parameters from an html form" $
request methodPost "/rpc/singlejsonparam" request methodPost "/rpc/singlejsonparam"
@@ -593,29 +589,33 @@ spec = do
"boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith` "boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith`
[json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun" [json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun"
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |] , "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
{ matchHeaders = [matchContentTypeJson] }
describe "weird requests" $ do describe "weird requests" $ do
it "can query as normal" $ do it "can query as normal" $ do
get "/Escap3e;" `shouldRespondWith` get "/Escap3e;" `shouldRespondWith`
[json| [{"so6meIdColumn":1},{"so6meIdColumn":2},{"so6meIdColumn":3},{"so6meIdColumn":4},{"so6meIdColumn":5}] |] [json| [{"so6meIdColumn":1},{"so6meIdColumn":2},{"so6meIdColumn":3},{"so6meIdColumn":4},{"so6meIdColumn":5}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/ghostBusters" `shouldRespondWith` get "/ghostBusters" `shouldRespondWith`
[json| [{"escapeId":1},{"escapeId":3},{"escapeId":5}] |] [json| [{"escapeId":1},{"escapeId":3},{"escapeId":5}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will embed a collection" $ it "will embed a collection" $
get "/Escap3e;?select=ghostBusters{*}" `shouldRespondWith` get "/Escap3e;?select=ghostBusters{*}" `shouldRespondWith`
[json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |] [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will embed using a column" $ it "will embed using a column" $
get "/ghostBusters?select=escapeId{*}" `shouldRespondWith` get "/ghostBusters?select=escapeId{*}" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |] [json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
describe "binary output" $ do describe "binary output" $ do
it "can query if a single column is selected" $ it "can query if a single column is selected" $
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") "" request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
matchBody = Just "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC" { matchStatus = 200
, matchStatus = 200 , matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
} }
it "fails if a single column is not selected" $ do it "fails if a single column is not selected" $ do
@@ -626,12 +626,9 @@ spec = do
request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") "" request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` 406 `shouldRespondWith` 406
it "concatenates results if more than one row is returned" $ it "concatenates results if more than one row is returned" $
request methodGet "/images_base64?select=img&name=in.A.png,B.png" (acceptHdrs "application/octet-stream") "" request methodGet "/images_base64?select=img&name=in.A.png,B.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
matchBody = Just "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=" { matchStatus = 200
, matchStatus = 200 , matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
} }
+33 -56
View File
@@ -31,19 +31,12 @@ 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" $
request methodPost "/rpc/getitemrange" [] emptyRange request methodPost "/rpc/getitemrange" [] emptyRange
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
matchBody = Just [json| [] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $ it "returns range Content-Range with range/*" $
request methodPost "/rpc/getitemrange" [] defaultRange request methodPost "/rpc/getitemrange" [] defaultRange
`shouldRespondWith` ResponseMatcher { `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}] |]
matchBody = Just [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}] |] { matchHeaders = ["Content-Range" <:> "0-14/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
context "with range headers" $ do context "with range headers" $ do
@@ -64,9 +57,8 @@ spec = do
it "returns an empty body when there are no results" $ it "returns an empty body when there are no results" $
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 1) emptyRange (rangeHdrs $ ByteRangeFromTo 0 1) emptyRange
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]" { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -95,18 +87,16 @@ spec = do
it "refuses a range with nonzero start when there are no items" $ it "refuses a range with nonzero start when there are no items" $
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange (rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Nothing { matchStatus = 416
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"] , matchHeaders = ["Content-Range" <:> "*/0"]
} }
it "refuses a range requesting start past last item" $ it "refuses a range requesting start past last item" $
request methodPost "/rpc/getitemrange" request methodPost "/rpc/getitemrange"
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange (rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Nothing { matchStatus = 416
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"] , matchHeaders = ["Content-Range" <:> "*/15"]
} }
@@ -120,68 +110,58 @@ spec = do
it "returns range Content-Range with /*" $ it "returns range Content-Range with /*" $
request methodGet "/menagerie" request methodGet "/menagerie"
[("Prefer", "count=none")] "" [("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]" { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "returns range Content-Range with range/*" $ it "returns range Content-Range with range/*" $
request methodGet "/items?order=id" request methodGet "/items?order=id"
[("Prefer", "count=none")] "" [("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher { `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}] |]
matchBody = Just [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}] |] { matchHeaders = ["Content-Range" <:> "0-14/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
it "returns range Content-Range with range/* even using other filters" $ it "returns range Content-Range with range/* even using other filters" $
request methodGet "/items?id=eq.1&order=id" request methodGet "/items?id=eq.1&order=id"
[("Prefer", "count=none")] "" [("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1}] |]
matchBody = Just [json| [{"id":1}] |] { matchHeaders = ["Content-Range" <:> "0-0/*"] }
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "with limit/offset parameters" $ do context "with limit/offset parameters" $ do
it "no parameters return everything" $ it "no parameters return everything" $
get "/items?select=id&order=id.asc" get "/items?select=id&order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith`
matchBody = Just [str|[{"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}]|] [str|[{"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}]|]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"] , matchHeaders = ["Content-Range" <:> "0-14/*"]
} }
it "top level limit with parameter" $ it "top level limit with parameter" $
get "/items?select=id&order=id.asc&limit=3" get "/items?select=id&order=id.asc&limit=3"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":1},{"id":2},{"id":3}]|]
matchBody = Just [str|[{"id":1},{"id":2},{"id":3}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "headers override get parameters" $ it "headers override get parameters" $
request methodGet "/items?select=id&order=id.asc&limit=3" request methodGet "/items?select=id&order=id.asc&limit=3"
(rangeHdrs $ ByteRangeFromTo 0 1) "" (rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":1},{"id":2}]|]
matchBody = Just [str|[{"id":1},{"id":2}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "limit works on all levels" $ it "limit works on all levels" $
get "/clients?select=id,projects{id,tasks{id}}&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1" get "/clients?select=id,projects{id,tasks{id}}&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith`
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|] [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]
, matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
it "limit and offset works on first level" $ it "limit and offset works on first level" $
get "/items?select=id&order=id.asc&limit=3&offset=2" get "/items?select=id&order=id.asc&limit=3&offset=2"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":3},{"id":4},{"id":5}]|]
matchBody = Just [str|[{"id":3},{"id":4},{"id":5}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"] , matchHeaders = ["Content-Range" <:> "2-4/*"]
} }
@@ -204,9 +184,8 @@ spec = do
it "returns an empty body when there are no results" $ it "returns an empty body when there are no results" $
request methodGet "/menagerie" request methodGet "/menagerie"
(rangeHdrs $ ByteRangeFromTo 0 1) "" (rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Just "[]" { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -235,17 +214,15 @@ spec = do
it "refuses a range with nonzero start when there are no items" $ it "refuses a range with nonzero start when there are no items" $
request methodGet "/menagerie" request methodGet "/menagerie"
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) "" (rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Nothing { matchStatus = 416
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"] , matchHeaders = ["Content-Range" <:> "*/0"]
} }
it "refuses a range requesting start past last item" $ it "refuses a range requesting start past last item" $
request methodGet "/items" request methodGet "/items"
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) "" (rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` "[]"
matchBody = Nothing { matchStatus = 416
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"] , matchHeaders = ["Content-Range" <:> "*/15"]
} }
+8 -13
View File
@@ -86,16 +86,14 @@ spec =
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 101, address: "xxx" } ] |] [json| [ { id: 101, address: "xxx" } ] |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ""
matchBody = Just "" { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
-- and the element should exist -- and the element should exist
get "/addresses?id=eq.101" get "/addresses?id=eq.101"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [str|[{"id":101,"address":"xxx"}]|]
matchBody = Just [str|[{"id":101,"address":"xxx"}]|] { matchStatus = 200
, matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
@@ -113,9 +111,8 @@ spec =
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |] [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ""
matchBody = Just "" { matchStatus = 201
, matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
@@ -142,11 +139,9 @@ spec =
[("Prefer", "return=representation"), singular] "" [("Prefer", "return=representation"), singular] ""
`shouldRespondWith` 406 `shouldRespondWith` 406
-- the rows should not exist, either
get firstItems get firstItems
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]
matchBody = Nothing { matchStatus = 200
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-9/*"] , matchHeaders = ["Content-Range" <:> "0-9/*"]
} }
+3
View File
@@ -6,6 +6,8 @@ import Test.Hspec.Wai.JSON
import Network.Wai (Application) import Network.Wai (Application)
import Control.Monad (void) import Control.Monad (void)
import SpecHelper
import Protolude hiding (get) import Protolude hiding (get)
spec :: SpecWith Application spec :: SpecWith Application
@@ -20,3 +22,4 @@ spec =
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF" get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
`shouldRespondWith` [json| [{ "هویت": 1 }] |] `shouldRespondWith` [json| [{ "هویت": 1 }] |]
{ matchHeaders = [matchContentTypeJson] }
+4 -1
View File
@@ -25,10 +25,13 @@ import Network.Wai.Test (SResponse(simpleStatus, simpleHeaders, simpleBody))
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Aeson (decode, Value(..)) import Data.Aeson (decode, Value(..))
import qualified Data.JsonSchema.Draft4 as D4 import qualified JSONSchema.Draft4 as D4
import Protolude import Protolude
matchContentTypeJson :: MatchHeader
matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
validateOpenApiResponse :: [Header] -> WaiSession () validateOpenApiResponse :: [Header] -> WaiSession ()
validateOpenApiResponse headers = do validateOpenApiResponse headers = do
r <- request methodGet "/" headers "" r <- request methodGet "/" headers ""