diff --git a/postgrest.cabal b/postgrest.cabal index 9559b7aff..0c546fe30 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -158,6 +158,8 @@ test-suite spec Feature.JsonOperatorSpec Feature.NoJwtSpec Feature.NonexistentSchemaSpec + Feature.OpenApiSpec + Feature.OptionsSpec Feature.ProxySpec Feature.QueryLimitedSpec Feature.QuerySpec @@ -166,7 +168,6 @@ test-suite spec Feature.RpcPreRequestGucsSpec Feature.RpcSpec Feature.SingularSpec - Feature.StructureSpec Feature.UnicodeSpec Feature.UpdateSpec Feature.UpsertSpec diff --git a/test/Feature/OpenApiSpec.hs b/test/Feature/OpenApiSpec.hs new file mode 100644 index 000000000..a5cf31070 --- /dev/null +++ b/test/Feature/OpenApiSpec.hs @@ -0,0 +1,504 @@ +module Feature.OpenApiSpec where + +import Control.Lens ((^?)) +import Data.Aeson.Types (Value (..)) +import Network.Wai (Application) +import Network.Wai.Test (SResponse (..)) + +import Data.Aeson.Lens +import Data.Aeson.QQ +import Network.HTTP.Types +import Test.Hspec hiding (pendingWith) +import Test.Hspec.Wai + +import PostgREST.Config (docsVersion) +import Protolude hiding (get) +import SpecHelper + +spec :: SpecWith ((), Application) +spec = describe "OpenAPI" $ do + it "root path returns a valid openapi spec" $ do + validateOpenApiResponse [("Accept", "application/openapi+json")] + request methodHead "/" (acceptHdrs "application/openapi+json") "" + `shouldRespondWith` "" { matchStatus = 200 } + + it "should respond to openapi request on none root path with 415" $ + request methodGet "/items" + (acceptHdrs "application/openapi+json") "" + `shouldRespondWith` 415 + + it "includes postgrest.org current version api docs" $ do + r <- simpleBody <$> get "/" + + let docsUrl = r ^? key "externalDocs" . key "url" + + liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/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" + postParameters = r ^? method "post" . 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" } + ] + |] + + postParameters `shouldBe` Just + [aesonQQ| + [ + { "$ref": "#/parameters/body.child_entities" }, + { "$ref": "#/parameters/select" }, + { "$ref": "#/parameters/preferReturn" } + ] + |] + + postResponse `shouldBe` Just "Created" + + patchResponse `shouldBe` Just "No Content" + + deleteResponse `shouldBe` Just "No Content" + + it "includes an array type for GET responses" $ do + r <- simpleBody <$> get "/" + + let childGetSchema = r ^? key "paths" + . key "/child_entities" + . key "get" + . key "responses" + . key "200" + . key "schema" + + liftIO $ + childGetSchema `shouldBe` Just + [aesonQQ| + { + "items": { + "$ref": "#/definitions/child_entities" + }, + "type": "array" + } + |] + + 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. Can be longer than sixty-three characters long", + "format": "text", + "type": "string" + }, + "parent_id": { + "description": "Note:\nThis is a Foreign Key to `entities.id`.", + "format": "integer", + "type": "integer" + } + }, + "required": [ + "id" + ] + } + |] + + 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 "Foreign table" $ + + it "includes foreign table properties" $ do + r <- simpleBody <$> get "/" + + let method s = key "paths" . key "/projects_dump" . key s + getSummary = r ^? method "get" . key "summary" + getDescription = r ^? method "get" . key "description" + getParameters = r ^? method "get" . key "parameters" + + liftIO $ do + + getSummary `shouldBe` Just "A temporary projects dump" + + getDescription `shouldBe` Just "Just a test for foreign tables" + + getParameters `shouldBe` Just + [aesonQQ| + [ + { "$ref": "#/parameters/rowFilter.projects_dump.id" }, + { "$ref": "#/parameters/rowFilter.projects_dump.name" }, + { "$ref": "#/parameters/rowFilter.projects_dump.client_id" }, + { "$ref": "#/parameters/select" }, + { "$ref": "#/parameters/order" }, + { "$ref": "#/parameters/range" }, + { "$ref": "#/parameters/rangeUnit" }, + { "$ref": "#/parameters/offset" }, + { "$ref": "#/parameters/limit" }, + { "$ref": "#/parameters/preferCount" } + ] + |] + + describe "Materialized view" $ + + it "includes materialized view properties" $ do + r <- simpleBody <$> get "/" + + let method s = key "paths" . key "/materialized_projects" . key s + summary = r ^? method "get" . key "summary" + description = r ^? method "get" . key "description" + parameters = r ^? method "get" . key "parameters" + + liftIO $ do + + summary `shouldBe` Just "A materialized view for projects" + + description `shouldBe` Just "Just a test for materialized views" + + parameters `shouldBe` Just + [aesonQQ| + [ + { "$ref": "#/parameters/rowFilter.materialized_projects.id" }, + { "$ref": "#/parameters/rowFilter.materialized_projects.name" }, + { "$ref": "#/parameters/rowFilter.materialized_projects.client_id" }, + { "$ref": "#/parameters/select" }, + { "$ref": "#/parameters/order" }, + { "$ref": "#/parameters/range" }, + { "$ref": "#/parameters/rangeUnit" }, + { "$ref": "#/parameters/offset" }, + { "$ref": "#/parameters/limit" }, + { "$ref": "#/parameters/preferCount" } + ] + |] + + describe "VIEW that has a source FK based on a UNIQUE key" $ + + it "includes fk description" $ do + r <- simpleBody <$> get "/" + + let referralLink = r ^? key "definitions" . key "referrals" . key "properties" . key "link" + + liftIO $ + referralLink `shouldBe` Just + [aesonQQ| + { + "format": "integer", + "type": "integer", + "description": "Note:\nThis is a Foreign Key to `pages.link`." + } + |] + + describe "PostgreSQL to Swagger Type Mapping" $ do + + it "character varying to string" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character_varying" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "character varying", + "type": "string" + } + |] + it "character(1) to string" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "maxLength": 1, + "format": "character", + "type": "string" + } + |] + + it "text to string" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "text", + "type": "string" + } + |] + + it "boolean to boolean" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_boolean" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "boolean", + "type": "boolean" + } + |] + + it "smallint to integer" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_smallint" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "smallint", + "type": "integer" + } + |] + + it "integer to integer" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_integer" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "integer", + "type": "integer" + } + |] + + it "bigint to integer" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "bigint", + "type": "integer" + } + |] + + it "numeric to number" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "numeric", + "type": "number" + } + |] + + it "real to number" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_real" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "real", + "type": "number" + } + |] + + it "double_precision to number" $ do + r <- simpleBody <$> get "/" + + let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_double_precision" + + liftIO $ + + types `shouldBe` Just + [aesonQQ| + { + "format": "double precision", + "type": "number" + } + |] + + describe "RPC" $ do + + it "includes function summary/description and body schema for arguments" $ do + r <- simpleBody <$> get "/" + + let method s = key "paths" . key "/rpc/varied_arguments" . key s + args = r ^? method "post" . key "parameters" . nth 0 . key "schema" + summary = r ^? method "post" . key "summary" + description = r ^? method "post" . key "description" + + liftIO $ do + + summary `shouldBe` Just "An RPC function" + + description `shouldBe` Just "Just a test for RPC function arguments" + + args `shouldBe` Just + [aesonQQ| + { + "required": [ + "double", + "varchar", + "boolean", + "date", + "money", + "enum", + "arr" + ], + "properties": { + "double": { + "format": "double precision", + "type": "number" + }, + "varchar": { + "format": "character varying", + "type": "string" + }, + "boolean": { + "format": "boolean", + "type": "boolean" + }, + "date": { + "format": "date", + "type": "string" + }, + "money": { + "format": "money", + "type": "string" + }, + "enum": { + "format": "enum_menagerie_type", + "type": "string" + }, + "arr": { + "format": "text[]", + "type": "string" + }, + "integer": { + "format": "integer", + "type": "integer" + }, + "json": { + "format": "json", + "type": "string" + }, + "jsonb": { + "format": "jsonb", + "type": "string" + } + }, + "type": "object", + "description": "An RPC function\n\nJust a test for RPC function arguments" + } + |] + + 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"]|] + diff --git a/test/Feature/OptionsSpec.hs b/test/Feature/OptionsSpec.hs new file mode 100644 index 000000000..b4dd89d64 --- /dev/null +++ b/test/Feature/OptionsSpec.hs @@ -0,0 +1,26 @@ +module Feature.OptionsSpec where + +import Network.Wai (Application) +import Network.Wai.Test (SResponse (..)) + +import Network.HTTP.Types +import Test.Hspec +import Test.Hspec.Wai + +import Protolude +import SpecHelper + +spec :: SpecWith ((), Application) +spec = 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" + diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs deleted file mode 100644 index c288edb8f..000000000 --- a/test/Feature/StructureSpec.hs +++ /dev/null @@ -1,521 +0,0 @@ -module Feature.StructureSpec where - -import Control.Lens ((^?)) -import Data.Aeson.Types (Value (..)) -import Network.Wai (Application) -import Network.Wai.Test (SResponse (..)) - -import Data.Aeson.Lens -import Data.Aeson.QQ -import Network.HTTP.Types -import Test.Hspec hiding (pendingWith) -import Test.Hspec.Wai - - - -import PostgREST.Config (docsVersion) -import Protolude hiding (get) -import SpecHelper - -spec :: SpecWith ((), Application) -spec = do - - describe "OpenAPI" $ do - it "root path returns a valid openapi spec" $ do - validateOpenApiResponse [("Accept", "application/openapi+json")] - request methodHead "/" (acceptHdrs "application/openapi+json") "" - `shouldRespondWith` "" { matchStatus = 200 } - - it "should respond to openapi request on none root path with 415" $ - request methodGet "/items" - (acceptHdrs "application/openapi+json") "" - `shouldRespondWith` 415 - - it "includes postgrest.org current version api docs" $ do - r <- simpleBody <$> get "/" - - let docsUrl = r ^? key "externalDocs" . key "url" - - liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/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" - postParameters = r ^? method "post" . 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" } - ] - |] - - postParameters `shouldBe` Just - [aesonQQ| - [ - { "$ref": "#/parameters/body.child_entities" }, - { "$ref": "#/parameters/select" }, - { "$ref": "#/parameters/preferReturn" } - ] - |] - - postResponse `shouldBe` Just "Created" - - patchResponse `shouldBe` Just "No Content" - - deleteResponse `shouldBe` Just "No Content" - - it "includes an array type for GET responses" $ do - r <- simpleBody <$> get "/" - - let childGetSchema = r ^? key "paths" - . key "/child_entities" - . key "get" - . key "responses" - . key "200" - . key "schema" - - liftIO $ - childGetSchema `shouldBe` Just - [aesonQQ| - { - "items": { - "$ref": "#/definitions/child_entities" - }, - "type": "array" - } - |] - - 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. Can be longer than sixty-three characters long", - "format": "text", - "type": "string" - }, - "parent_id": { - "description": "Note:\nThis is a Foreign Key to `entities.id`.", - "format": "integer", - "type": "integer" - } - }, - "required": [ - "id" - ] - } - |] - - 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 "Foreign table" $ - - it "includes foreign table properties" $ do - r <- simpleBody <$> get "/" - - let method s = key "paths" . key "/projects_dump" . key s - getSummary = r ^? method "get" . key "summary" - getDescription = r ^? method "get" . key "description" - getParameters = r ^? method "get" . key "parameters" - - liftIO $ do - - getSummary `shouldBe` Just "A temporary projects dump" - - getDescription `shouldBe` Just "Just a test for foreign tables" - - getParameters `shouldBe` Just - [aesonQQ| - [ - { "$ref": "#/parameters/rowFilter.projects_dump.id" }, - { "$ref": "#/parameters/rowFilter.projects_dump.name" }, - { "$ref": "#/parameters/rowFilter.projects_dump.client_id" }, - { "$ref": "#/parameters/select" }, - { "$ref": "#/parameters/order" }, - { "$ref": "#/parameters/range" }, - { "$ref": "#/parameters/rangeUnit" }, - { "$ref": "#/parameters/offset" }, - { "$ref": "#/parameters/limit" }, - { "$ref": "#/parameters/preferCount" } - ] - |] - - describe "Materialized view" $ - - it "includes materialized view properties" $ do - r <- simpleBody <$> get "/" - - let method s = key "paths" . key "/materialized_projects" . key s - summary = r ^? method "get" . key "summary" - description = r ^? method "get" . key "description" - parameters = r ^? method "get" . key "parameters" - - liftIO $ do - - summary `shouldBe` Just "A materialized view for projects" - - description `shouldBe` Just "Just a test for materialized views" - - parameters `shouldBe` Just - [aesonQQ| - [ - { "$ref": "#/parameters/rowFilter.materialized_projects.id" }, - { "$ref": "#/parameters/rowFilter.materialized_projects.name" }, - { "$ref": "#/parameters/rowFilter.materialized_projects.client_id" }, - { "$ref": "#/parameters/select" }, - { "$ref": "#/parameters/order" }, - { "$ref": "#/parameters/range" }, - { "$ref": "#/parameters/rangeUnit" }, - { "$ref": "#/parameters/offset" }, - { "$ref": "#/parameters/limit" }, - { "$ref": "#/parameters/preferCount" } - ] - |] - - describe "VIEW that has a source FK based on a UNIQUE key" $ - - it "includes fk description" $ do - r <- simpleBody <$> get "/" - - let referralLink = r ^? key "definitions" . key "referrals" . key "properties" . key "link" - - liftIO $ - referralLink `shouldBe` Just - [aesonQQ| - { - "format": "integer", - "type": "integer", - "description": "Note:\nThis is a Foreign Key to `pages.link`." - } - |] - - describe "PostgreSQL to Swagger Type Mapping" $ do - - it "character varying to string" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character_varying" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "character varying", - "type": "string" - } - |] - it "character(1) to string" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_character" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "maxLength": 1, - "format": "character", - "type": "string" - } - |] - - it "text to string" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_text" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "text", - "type": "string" - } - |] - - it "boolean to boolean" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_boolean" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "boolean", - "type": "boolean" - } - |] - - it "smallint to integer" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_smallint" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "smallint", - "type": "integer" - } - |] - - it "integer to integer" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_integer" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "integer", - "type": "integer" - } - |] - - it "bigint to integer" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_bigint" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "bigint", - "type": "integer" - } - |] - - it "numeric to number" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_numeric" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "numeric", - "type": "number" - } - |] - - it "real to number" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_real" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "real", - "type": "number" - } - |] - - it "double_precision to number" $ do - r <- simpleBody <$> get "/" - - let types = r ^? key "definitions" . key "openapi_types" . key "properties" . key "a_double_precision" - - liftIO $ - - types `shouldBe` Just - [aesonQQ| - { - "format": "double precision", - "type": "number" - } - |] - - describe "RPC" $ do - - it "includes function summary/description and body schema for arguments" $ do - r <- simpleBody <$> get "/" - - let method s = key "paths" . key "/rpc/varied_arguments" . key s - args = r ^? method "post" . key "parameters" . nth 0 . key "schema" - summary = r ^? method "post" . key "summary" - description = r ^? method "post" . key "description" - - liftIO $ do - - summary `shouldBe` Just "An RPC function" - - description `shouldBe` Just "Just a test for RPC function arguments" - - args `shouldBe` Just - [aesonQQ| - { - "required": [ - "double", - "varchar", - "boolean", - "date", - "money", - "enum", - "arr" - ], - "properties": { - "double": { - "format": "double precision", - "type": "number" - }, - "varchar": { - "format": "character varying", - "type": "string" - }, - "boolean": { - "format": "boolean", - "type": "boolean" - }, - "date": { - "format": "date", - "type": "string" - }, - "money": { - "format": "money", - "type": "string" - }, - "enum": { - "format": "enum_menagerie_type", - "type": "string" - }, - "arr": { - "format": "text[]", - "type": "string" - }, - "integer": { - "format": "integer", - "type": "integer" - }, - "json": { - "format": "json", - "type": "string" - }, - "jsonb": { - "format": "jsonb", - "type": "string" - } - }, - "type": "object", - "description": "An RPC function\n\nJust a test for RPC function arguments" - } - |] - - 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" diff --git a/test/Main.hs b/test/Main.hs index 5d5a964fa..2e561b7a7 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -36,6 +36,8 @@ import qualified Feature.JsonOperatorSpec import qualified Feature.MultipleSchemaSpec import qualified Feature.NoJwtSpec import qualified Feature.NonexistentSchemaSpec +import qualified Feature.OpenApiSpec +import qualified Feature.OptionsSpec import qualified Feature.ProxySpec import qualified Feature.QueryLimitedSpec import qualified Feature.QuerySpec @@ -45,7 +47,6 @@ import qualified Feature.RootSpec import qualified Feature.RpcPreRequestGucsSpec import qualified Feature.RpcSpec import qualified Feature.SingularSpec -import qualified Feature.StructureSpec import qualified Feature.UnicodeSpec import qualified Feature.UpdateSpec import qualified Feature.UpsertSpec @@ -104,10 +105,11 @@ main = do , ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec) , ("Feature.CorsSpec" , Feature.CorsSpec.spec) , ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion) + , ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec) + , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec) , ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion) , ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec) , ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion) - , ("Feature.StructureSpec" , Feature.StructureSpec.spec) , ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion) , ("Feature.UpsertSpec" , Feature.UpsertSpec.spec) ]