packages feed

postgrest-8.0.0: test/Feature/SingularSpec.hs

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