module Feature.StructureSpec where import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai import Network.HTTP.Types import PostgREST.Config (docsVersion) import Control.Lens ((^?)) import Data.Aeson.Types (Value (..)) import Data.Aeson.Lens import Data.Aeson.QQ import SpecHelper import Network.Wai (Application) import Network.Wai.Test (SResponse(..)) import Protolude hiding (get) spec :: SpecWith Application spec = do describe "OpenAPI" $ do it "root path returns a valid openapi spec" $ validateOpenApiResponse [("Accept", "application/openapi+json")] it "should respond to openapi request on none root path with 415" $ request methodGet "/items" (acceptHdrs "application/openapi+json") "" `shouldRespondWith` 415 it "includes postgrest.com current version api docs" $ do r <- simpleBody <$> get "/" let docsUrl = r ^? key "externalDocs" . key "url" liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.com/en/" <> docsVersion <> "/api.html")) describe "table" $ do it "includes paths to tables" $ do r <- simpleBody <$> get "/" let method s = key "paths" . key "/child_entities" . key s childGetSummary = r ^? method "get" . key "summary" childGetDescription = r ^? method "get" . key "description" getParameters = r ^? method "get" . key "parameters" postResponse = r ^? method "post" . key "responses" . key "201" . key "description" patchResponse = r ^? method "patch" . key "responses" . key "204" . key "description" deleteResponse = r ^? method "delete" . key "responses" . key "204" . key "description" let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s grandChildGetSummary = r ^? grandChildGet "summary" grandChildGetDescription = r ^? grandChildGet "description" liftIO $ do childGetSummary `shouldBe` Just "child_entities comment" childGetDescription `shouldBe` Nothing grandChildGetSummary `shouldBe` Just "grandchild_entities summary" grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines" getParameters `shouldBe` Just [aesonQQ| [ { "$ref": "#/parameters/rowFilter.child_entities.id" }, { "$ref": "#/parameters/rowFilter.child_entities.name" }, { "$ref": "#/parameters/rowFilter.child_entities.parent_id" }, { "$ref": "#/parameters/select" }, { "$ref": "#/parameters/order" }, { "$ref": "#/parameters/range" }, { "$ref": "#/parameters/rangeUnit" }, { "$ref": "#/parameters/offset" }, { "$ref": "#/parameters/limit" }, { "$ref": "#/parameters/preferCount" } ] |] postResponse `shouldBe` Just "Created" patchResponse `shouldBe` Just "No Content" deleteResponse `shouldBe` Just "No Content" it "includes definitions to tables" $ do r <- simpleBody <$> get "/" let def = r ^? key "definitions" . key "child_entities" liftIO $ def `shouldBe` Just [aesonQQ| { "type": "object", "description": "child_entities comment", "properties": { "id": { "description": "child_entities id comment\n\nNote:\nThis is a Primary Key.", "format": "integer", "type": "integer" }, "name": { "description": "child_entities name comment", "format": "text", "type": "string" }, "parent_id": { "description": "Note:\nThis is a Foreign Key to `entities.id`.", "format": "integer", "type": "integer" } } } |] it "doesn't include privileged table for anonymous" $ do r <- simpleBody <$> get "/" let tablePath = r ^? key "paths" . key "/authors_only" liftIO $ tablePath `shouldBe` Nothing it "includes table if user has permission" $ do let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" r <- simpleBody <$> request methodGet "/" [auth] "" let tableTag = r ^? key "paths" . key "/authors_only" . key "post" . key "tags" . nth 0 liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|] describe "RPC" $ do it "includes body schema for arguments" $ do r <- simpleBody <$> get "/" let args = r ^? key "paths" . key "/rpc/varied_arguments" . key "post" . key "parameters" . nth 0 . key "schema" liftIO $ args `shouldBe` Just [aesonQQ| { "required": [ "double", "varchar", "boolean", "date", "money", "enum" ], "properties": { "double": { "format": "double precision", "type": "string" }, "varchar": { "format": "character varying", "type": "string" }, "boolean": { "format": "boolean", "type": "boolean" }, "date": { "format": "date", "type": "string" }, "money": { "format": "money", "type": "string" }, "enum": { "format": "test.enum_menagerie_type", "type": "string" }, "integer": { "format": "integer", "type": "integer" } }, "type": "object" } |] it "doesn't include privileged function for anonymous" $ do r <- simpleBody <$> get "/" let funcPath = r ^? key "paths" . key "/rpc/privileged_hello" liftIO $ funcPath `shouldBe` Nothing it "includes function if user has permission" $ do let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" r <- simpleBody <$> request methodGet "/" [auth] "" let funcTag = r ^? key "paths" . key "/rpc/privileged_hello" . key "post" . key "tags" . nth 0 liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|] it "doesn't include OUT params of function as required parameters" $ do r <- simpleBody <$> get "/" let params = r ^? key "paths" . key "/rpc/many_out_params" . key "post" . key "parameters" . nth 0 . key "schema". key "required" liftIO $ params `shouldBe` Nothing it "includes INOUT params(with no DEFAULT) of function as required parameters" $ do r <- simpleBody <$> get "/" let params = r ^? key "paths" . key "/rpc/many_inout_params" . key "post" . key "parameters" . nth 0 . key "schema". key "required" liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|] describe "Allow header" $ do it "includes read/write verbs for writeable table" $ do r <- request methodOptions "/items" [] "" liftIO $ simpleHeaders r `shouldSatisfy` matchHeader "Allow" "GET,POST,PATCH,DELETE" it "includes read verbs for read-only table" $ do r <- request methodOptions "/has_count_column" [] "" liftIO $ simpleHeaders r `shouldSatisfy` matchHeader "Allow" "GET"