module Feature.SingularSpec where import Network.Wai (Application) import Network.Wai.Test (SResponse (..)) import Network.HTTP.Types import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Protolude hiding (get) import SpecHelper spec :: SpecWith ((), Application) spec = describe "Requesting singular json object" $ do let singular = ("Accept", "application/vnd.pgrst.object+json") context "with GET request" $ do it "fails for zero rows" $ request methodGet "/items?id=gt.0&id=lt.0" [singular] "" `shouldRespondWith` 406 it "will select an existing object" $ do request methodGet "/items?id=eq.5" [singular] "" `shouldRespondWith` [json|{"id":5}|] { matchHeaders = [matchContentTypeSingular] } -- also test without the +json suffix request methodGet "/items?id=eq.5" [("Accept", "application/vnd.pgrst.object")] "" `shouldRespondWith` [json|{"id":5}|] { matchHeaders = [matchContentTypeSingular] } it "can combine multiple prefer values" $ request methodGet "/items?id=eq.5" [singular, ("Prefer","count=none")] "" `shouldRespondWith` [json|{"id":5}|] { matchHeaders = [matchContentTypeSingular] } it "can shape plurality singular object routes" $ request methodGet "/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)" [singular] "" `shouldRespondWith` [json|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|] { matchHeaders = [matchContentTypeSingular] } context "when updating rows" $ do it "works for one row with return=rep" $ do request methodPatch "/addresses?id=eq.1" [("Prefer", "return=representation"), singular] [json| { address: "B Street" } |] `shouldRespondWith` [json|{"id":1,"address":"B Street"}|] { matchHeaders = [matchContentTypeSingular] } it "works for one row with return=minimal" $ request methodPatch "/addresses?id=eq.1" [("Prefer", "return=minimal"), singular] [json| { address: "C Street" } |] `shouldRespondWith` "" { matchStatus = 204 } it "raises an error for multiple rows" $ do request methodPatch "/addresses" [("Prefer", "tx=commit"), singular] [json| { address: "zzz" } |] `shouldRespondWith` [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should not be updated, either get "/addresses?id=eq.1" `shouldRespondWith` [json|[{"id":1,"address":"address 1"}]|] it "raises an error for multiple rows with return=rep" $ do request methodPatch "/addresses" [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] [json| { address: "zzz" } |] `shouldRespondWith` [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should not be updated, either get "/addresses?id=eq.1" `shouldRespondWith` [json|[{"id":1,"address":"address 1"}]|] it "raises an error for zero rows" $ request methodPatch "/items?id=gt.0&id=lt.0" [singular] [json|{"id":1}|] `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } it "raises an error for zero rows with return=rep" $ request methodPatch "/items?id=gt.0&id=lt.0" [("Prefer", "return=representation"), singular] [json|{"id":1}|] `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } context "when creating rows" $ do it "works for one row with return=rep" $ do request methodPost "/addresses" [("Prefer", "return=representation"), singular] [json| [ { id: 102, address: "xxx" } ] |] `shouldRespondWith` [json|{"id":102,"address":"xxx"}|] { matchStatus = 201 , matchHeaders = [matchContentTypeSingular] } it "works for one row with return=minimal" $ do request methodPost "/addresses" [("Prefer", "return=minimal"), singular] [json| [ { id: 103, address: "xxx" } ] |] `shouldRespondWith` "" { matchStatus = 201 , matchHeaders = ["Content-Range" <:> "*/*"] } it "raises an error when attempting to create multiple entities" $ do request methodPost "/addresses" [("Prefer", "tx=commit"), singular] [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |] `shouldRespondWith` [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should not exist, either get "/addresses?id=eq.200" `shouldRespondWith` "[]" it "raises an error when attempting to create multiple entities with return=rep" $ do request methodPost "/addresses" [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] [json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |] `shouldRespondWith` [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should not exist, either get "/addresses?id=eq.202" `shouldRespondWith` "[]" it "raises an error regardless of return=minimal" $ do request methodPost "/addresses" [("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular] [json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |] `shouldRespondWith` [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should not exist, either get "/addresses?id=eq.204" `shouldRespondWith` "[]" it "raises an error when creating zero entities" $ request methodPost "/addresses" [singular] [json| [ ] |] `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } it "raises an error when creating zero entities with return=rep" $ request methodPost "/addresses" [("Prefer", "return=representation"), singular] [json| [ ] |] `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } context "when deleting rows" $ do it "works for one row with return=rep" $ do p <- request methodDelete "/items?id=eq.11" [("Prefer", "return=representation"), singular] "" liftIO $ simpleBody p `shouldBe` [json|{"id":11}|] it "works for one row with return=minimal" $ do p <- request methodDelete "/items?id=eq.12" [("Prefer", "return=minimal"), singular] "" liftIO $ simpleBody p `shouldBe` "" it "raises an error when attempting to delete multiple entities" $ do request methodDelete "/items?id=gt.0&id=lt.6" [("Prefer", "tx=commit"), singular] "" `shouldRespondWith` [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should still exist get "/items?id=gt.0&id=lt.6&order=id" `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |] { matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-4/*"] } it "raises an error when attempting to delete multiple entities with return=rep" $ do request methodDelete "/items?id=gt.5&id=lt.11" [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] "" `shouldRespondWith` [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- the rows should still exist get "/items?id=gt.5&id=lt.11" `shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |] { matchStatus = 200 , matchHeaders = ["Content-Range" <:> "0-4/*"] } it "raises an error when deleting zero entities" $ request methodDelete "/items?id=lt.0" [singular] "" `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } it "raises an error when deleting zero entities with return=rep" $ request methodDelete "/items?id=lt.0" [("Prefer", "return=representation"), singular] "" `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } context "when calling a stored proc" $ do it "fails for zero rows" $ request methodPost "/rpc/getproject" [singular] [json|{ "id": 9999999}|] `shouldRespondWith` [json|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } -- this one may be controversial, should vnd.pgrst.object include -- the likes of 2 and "hello?" it "succeeds for scalar result" $ request methodPost "/rpc/sayhello" [singular] [json|{ "name": "world"}|] `shouldRespondWith` 200 it "returns a single object for json proc" $ request methodPost "/rpc/getproject" [singular] [json|{ "id": 1}|] `shouldRespondWith` [json|{"id":1,"name":"Windows 7","client_id":1}|] { matchHeaders = [matchContentTypeSingular] } it "fails for multiple rows" $ request methodPost "/rpc/getallprojects" [singular] "{}" `shouldRespondWith` [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [matchContentTypeSingular] } it "fails for multiple rows with rolled back changes" $ do post "/rpc/getproject?select=id,name" [json| {"id": 1} |] `shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|] request methodPost "/rpc/setprojects" [("Prefer", "tx=commit"), singular] [json| {"id_l": 1, "id_h": 2, "name": "changed"} |] `shouldRespondWith` [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] { matchStatus = 406 , matchHeaders = [ matchContentTypeSingular , "Preference-Applied" <:> "tx=commit" ] } -- should rollback function post "/rpc/getproject?select=id,name" [json| {"id": 1} |] `shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|]