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