packages feed

postgrest-9.0.1: test/spec/Feature/MultipleSchemaSpec.hs

module Feature.MultipleSchemaSpec where

import Control.Lens    ((^?))
import Data.Aeson.Lens
import Data.Aeson.QQ

import Network.HTTP.Types
import Network.Wai        (Application)
import Network.Wai.Test   (SResponse (simpleHeaders), simpleBody)

import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON

import Protolude
import SpecHelper

spec :: SpecWith ((), Application)
spec =
  describe "multiple schemas in single instance" $ do
    context "Reading tables on different schemas" $ do
      it "succeeds in reading table from default schema v1 if no schema is selected via header" $
        request methodGet "/parents" [] "" `shouldRespondWith`
          [json|[
            {"id":1,"name":"parent v1-1"},
            {"id":2,"name":"parent v1-2"}
          ]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
          }

      it "succeeds in reading table from default schema v1 after explicitly passing it in the header" $
        request methodGet "/parents" [("Accept-Profile", "v1")] "" `shouldRespondWith`
          [json|[
            {"id":1,"name":"parent v1-1"},
            {"id":2,"name":"parent v1-2"}
          ]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
          }

      it "succeeds in reading table from schema v2" $
        request methodGet "/parents" [("Accept-Profile", "v2")] "" `shouldRespondWith`
          [json|[
            {"id":3,"name":"parent v2-3"},
            {"id":4,"name":"parent v2-4"}
          ]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

      it "succeeds in reading another_table from schema v2" $
        request methodGet "/another_table" [("Accept-Profile", "v2")] "" `shouldRespondWith`
          [json|[
            {"id":5,"another_value":"value 5"},
            {"id":6,"another_value":"value 6"}
          ]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

      it "doesn't find another_table in schema v1" $
        request methodGet "/another_table" [("Accept-Profile", "v1")] "" `shouldRespondWith` 404

      it "fails trying to read table from unkown schema" $
        request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
          [json|{"message":"The schema must be one of the following: v1, v2"}|]
          {
            matchStatus = 406
          }

    context "Inserting tables on different schemas" $ do
      it "succeeds inserting on default schema and returning it" $
        request methodPost "/children"
            [("Prefer", "return=representation")]
            [json|{"id": 0, "name": "child v1-1", "parent_id": 1}|]
          `shouldRespondWith`
            [json|[{"id": 0, "name": "child v1-1", "parent_id": 1}]|]
            {
              matchStatus = 201
            , matchHeaders = ["Content-Profile" <:> "v1"]
            }

      it "succeeds inserting on the v1 schema and returning its parent" $
        request methodPost "/children?select=id,parent(*)"
            [("Prefer", "return=representation"), ("Content-Profile", "v1")]
            [json|{"id": 0, "name": "child v1-2", "parent_id": 2}|]
          `shouldRespondWith`
            [json|[{"id": 0, "parent": {"id": 2, "name": "parent v1-2"}}]|]
            {
              matchStatus = 201
            , matchHeaders = ["Content-Profile" <:> "v1"]
            }

      it "succeeds inserting on the v2 schema and returning its parent" $
        request methodPost "/children?select=id,parent(*)"
            [("Prefer", "return=representation"), ("Content-Profile", "v2")]
            [json|{"id": 0, "name": "child v2-3", "parent_id": 3}|]
          `shouldRespondWith`
            [json|[{"id": 0, "parent": {"id": 3, "name": "parent v2-3"}}]|]
            {
              matchStatus = 201
            , matchHeaders = ["Content-Profile" <:> "v2"]
            }

      it "fails when inserting on an unknown schema" $
        request methodPost "/children" [("Content-Profile", "unknown")]
          [json|{"name": "child 4", "parent_id": 4}|]
          `shouldRespondWith`
          [json|{"message":"The schema must be one of the following: v1, v2"}|]
          {
            matchStatus = 406
          }

    context "calling procs on different schemas" $ do
      it "succeeds in calling the default schema proc" $
        request methodGet "/rpc/get_parents_below?id=6" [] ""
          `shouldRespondWith`
          [json|[{"id":1,"name":"parent v1-1"}, {"id":2,"name":"parent v1-2"}]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
          }

      it "succeeds in calling the v1 schema proc and embedding" $
        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""
          `shouldRespondWith`
          [json| [
            {"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},
            {"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
          }

      it "succeeds in calling the v2 schema proc and embedding" $
        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""
          `shouldRespondWith`
          [json| [
            {"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},
            {"id":4,"name":"parent v2-4","children":[]}] |]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

      it "succeeds in calling the v2 schema proc with POST by using Content-Profile" $
        request methodPost "/rpc/get_parents_below?select=id,name" [("Content-Profile", "v2")]
          [json|{"id": "6"}|]
          `shouldRespondWith`
          [json| [
            {"id":3,"name":"parent v2-3"},
            {"id":4,"name":"parent v2-4"}]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

    context "Modifying tables on different schemas" $ do
      it "succeeds in patching on the v1 schema and returning its parent" $
        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]
          [json|{"name": "child v1-1 updated"}|]
          `shouldRespondWith`
          [json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
          }

      it "succeeds in patching on the v2 schema and returning its parent" $
        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
          [json|{"name": "child v2-1 updated"}|]
          `shouldRespondWith`
          [json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

      it "succeeds on deleting on the v2 schema" $ do
        request methodDelete "/children?id=eq.1"
            [("Content-Profile", "v2"), ("Prefer", "return=representation")]
            ""
          `shouldRespondWith`
            [json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]
            { matchHeaders = ["Content-Profile" <:> "v2"] }

      it "succeeds on PUT on the v2 schema" $
        request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
          [json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
          `shouldRespondWith`
          [json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
          {
            matchStatus = 200
          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
          }

    context "OpenAPI output" $ do
      it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do
          req <- request methodGet "/" [] ""

          liftIO $ do
            simpleHeaders req `shouldSatisfy` matchHeader "Content-Profile" "v1"

            let def = simpleBody req ^? key "definitions" . key "parents"

            def `shouldBe` Just
                [aesonQQ|
                  {
                    "type" : "object",
                    "properties" : {
                      "id" : {
                        "description" : "Note:\nThis is a Primary Key.<pk/>",
                        "format" : "integer",
                        "type" : "integer"
                      },
                      "name" : {
                        "format" : "text",
                        "type" : "string"
                      }
                    },
                    "required" : [
                      "id"
                    ]
                  }
                |]

      it "succeeds in reading table definition from default schema v1 after explicitly passing it in the header" $ do
          r <- request methodGet "/" [("Accept-Profile", "v1")] ""

          liftIO $ do
            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v1"

            let def = simpleBody r ^? key "definitions" . key "parents"

            def `shouldBe` Just
                [aesonQQ|
                  {
                    "type" : "object",
                    "properties" : {
                      "id" : {
                        "description" : "Note:\nThis is a Primary Key.<pk/>",
                        "format" : "integer",
                        "type" : "integer"
                      },
                      "name" : {
                        "format" : "text",
                        "type" : "string"
                      }
                    },
                    "required" : [
                      "id"
                    ]
                  }
                |]

      it "succeeds in reading table definition from schema v2" $ do
          r <- request methodGet "/" [("Accept-Profile", "v2")] ""

          liftIO $ do
            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"

            let def = simpleBody r ^? key "definitions" . key "parents"

            def `shouldBe` Just
                [aesonQQ|
                  {
                    "type" : "object",
                    "properties" : {
                      "id" : {
                        "description" : "Note:\nThis is a Primary Key.<pk/>",
                        "format" : "integer",
                        "type" : "integer"
                      },
                      "name" : {
                        "format" : "text",
                        "type" : "string"
                      }
                    },
                    "required" : [
                      "id"
                    ]
                  }
                |]

      it "succeeds in reading another_table definition from schema v2" $ do
          r <- request methodGet "/" [("Accept-Profile", "v2")] ""

          liftIO $ do
            simpleHeaders r `shouldSatisfy` matchHeader "Content-Profile" "v2"

            let def = simpleBody r ^? key "definitions" . key "another_table"

            def `shouldBe` Just
                [aesonQQ|
                  {
                    "type" : "object",
                    "properties" : {
                      "id" : {
                        "description" : "Note:\nThis is a Primary Key.<pk/>",
                        "format" : "integer",
                        "type" : "integer"
                      },
                      "another_value" : {
                        "format" : "text",
                        "type" : "string"
                      }
                    },
                    "required" : [
                      "id"
                    ]
                  }
                |]

      it "doesn't find another_table definition in schema v1" $ do
        r <- request methodGet "/" [("Accept-Profile", "v1")] ""

        liftIO $ do
          let def = simpleBody r ^? key "definitions" . key "another_table"
          def `shouldBe` Nothing

      it "fails trying to read definitions from unkown schema" $
        request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
          [json|{"message":"The schema must be one of the following: v1, v2"}|]
          {
            matchStatus = 406
          }