{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# OPTIONS_GHC -Wno-missing-local-signatures #-} module Tests.IntrospectionSpec (main) where import Protolude ( Either (Right), Maybe (Nothing), Monoid (mempty), Text, readFile, void, ($), ) import Data.Aeson qualified as Ae import Database.SQLite.Simple qualified as SS import Database.SQLite.Simple.QQ (sql) import Language.GraphQL.Class (gql) import Language.GraphQL.JSON (graphql) import System.Directory (makeAbsolute) import System.FilePath ((</>)) import Test.Hspec (Spec, describe, it, shouldBe) import AirGQL.GraphQL (getDerivedSchema) import AirGQL.Lib (getEnrichedTables, insertOnly, writeOnly) import AirGQL.Raw (raw) import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf) import AirGQL.Utils (withRetryConn) import Tests.Utils (dbPath, fixtureDbId, rmSpaces, testRoot, unorderedShouldBe, withTestDbConn) main :: Spec main = void $ do describe "Query" $ do it "supports a minimal introspection query" $ do let introspectionQuery :: Text introspectionQuery = [gql| query IntrospectionQuery { __schema { queryType { name } } } |] expected = rmSpaces [raw| { "data": { "__schema": { "queryType": { "name": "Query" } } } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery Ae.encode result `shouldBe` expected it "supports an optional filter argument" $ do let introspectionQuery :: Text introspectionQuery = [gql| query IntrospectionQuery { __schema { queryType { name fields { name args { name type { name } } } } } } |] expected = rmSpaces [raw|{ "data": { "__schema": { "queryType": { "name": "Query", "fields": [ { "name": "users", "args": [ { "name": "where", "type": { "name": "users_filter" } }, { "name": "order_by", "type": { "name": null } }, { "name": "limit", "type": { "name": "Int" } }, { "name": "offset", "type": { "name": "Int" } } ] }, { "name": "songs", "args": [ { "name": "where", "type": { "name": "songs_filter" } }, { "name": "order_by", "type": { "name": null } }, { "name": "limit", "type": { "name": "Int" } }, { "name": "offset", "type": { "name": "Int" } } ] }, { "name": "users_by_pk", "args": [ { "name": "email", "type": { "name": null } } ] }, { "name": "songs_by_pk", "args": [ { "name": "rowid", "type": { "name": null } } ] } ] } } } }|] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery Ae.encode result `shouldBe` expected it "doesn't allow writeonly tokens to read data" $ do let introspectionQuery :: Text introspectionQuery = [gql| query IntrospectionQuery { __schema { queryType { name } } } |] expected = rmSpaces [raw| { "data": null, "errors": [{ "locations": [{ "column":3, "line":2 }], "message": "user error (Cannot read field using the provided token)", "path": ["__schema"] }] } |] schema <- withRetryConn dbPath $ \conn -> do Right tables <- getEnrichedTables conn getDerivedSchema defaultSchemaConf{accessMode = writeOnly} conn fixtureDbId tables Right response <- graphql schema Nothing mempty introspectionQuery Ae.encode response `shouldBe` expected describe "Mutation" $ do it "supports introspection queries" $ do let introspectionQuery :: Text introspectionQuery = [gql| query IntrospectionQuery { __schema { mutationType { name fields { name args { name } } } } } |] expected = [raw| { "data": { "__schema": { "mutationType": { "name": "Mutation", "fields": [ { "name": "insert_users", "args": [ { "name": "objects" }, { "name": "on_conflict" } ] }, { "name": "update_users", "args": [ { "name": "where" }, { "name": "_set" } ] }, { "name": "update_users_by_pk", "args": [ { "name": "email" }, { "name": "_set" } ] }, { "name": "delete_users", "args": [ { "name": "where" } ] }, { "name": "delete_users_by_pk", "args": [ { "name": "email" } ] }, { "name": "insert_songs", "args": [ { "name": "objects" }, { "name": "on_conflict" } ] }, { "name": "update_songs", "args": [ { "name": "where" }, { "name": "_set" } ] }, { "name": "update_songs_by_pk", "args": [ { "name": "rowid" }, { "name": "_set" } ] }, { "name": "delete_songs", "args": [ { "name": "where" } ] }, { "name": "delete_songs_by_pk", "args": [ { "name": "rowid" } ] } ] } } } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery result `unorderedShouldBe` expected it "supports __typename on root query" $ do let introspectionQuery = [gql| query TypeName { __typename } |] expected = rmSpaces [raw| { "data": { "__typename" : "Query" } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery Ae.encode result `shouldBe` expected it "returns fields for {query,mutation,subscription}Type" $ do let introspectionQuery = [gql| query { __schema { queryType { fields { name } } mutationType { fields { name } } subscriptionType { fields { name } } } } |] expected = [raw| { "data": { "__schema": { "queryType": { "fields": [ { "name": "users" }, { "name": "songs" }, { "name": "users_by_pk" }, { "name": "songs_by_pk" } ] }, "subscriptionType": null, "mutationType": { "fields": [ { "name": "insert_users" }, { "name": "update_users" }, { "name": "delete_users" }, { "name": "insert_songs" }, { "name": "update_songs" }, { "name": "delete_songs" }, { "name": "update_users_by_pk" }, { "name": "delete_users_by_pk" }, { "name": "update_songs_by_pk" }, { "name": "delete_songs_by_pk" } ] } } } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery result `unorderedShouldBe` expected it "supports __typename fields" $ do let introspectionQuery = [gql| query UsersTypeName { users { __typename } } |] expected = rmSpaces [raw| { "data": { "users": [ { "__typename" : "users_row" } ] } } |] conn <- SS.open dbPath SS.execute_ conn [sql| insert into users (name, email, created_utc) values ('John', 'john@example.com', '2022-01-01T00:00Z') |] Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery Ae.encode result `shouldBe` expected it "returns types" $ do let introspectionQuery = [gql| query { __schema { types { kind name } } } |] expected = [raw| { "data": { "__schema": { "types": [ { "kind": "OBJECT", "name": "users_row" }, { "kind": "OBJECT", "name": "users_mutation_response" }, { "kind": "OBJECT", "name": "users_mutation_by_pk_response" }, { "kind": "INPUT_OBJECT", "name": "users_insert_input" }, { "kind": "ENUM", "name": "users_column" }, { "kind": "INPUT_OBJECT", "name": "users_upsert_on_conflict" }, { "kind": "INPUT_OBJECT", "name": "users_set_input" }, { "kind": "INPUT_OBJECT", "name": "users_filter" }, { "kind": "INPUT_OBJECT", "name": "users_order_by" }, { "kind": "OBJECT", "name": "songs_row" }, { "kind": "OBJECT", "name": "songs_mutation_response" }, { "kind": "OBJECT", "name": "songs_mutation_by_pk_response" }, { "kind": "INPUT_OBJECT", "name": "songs_insert_input" }, { "kind": "ENUM", "name": "songs_column" }, { "kind": "INPUT_OBJECT", "name": "songs_upsert_on_conflict" }, { "kind": "INPUT_OBJECT", "name": "songs_set_input" }, { "kind": "INPUT_OBJECT", "name": "songs_filter" }, { "kind": "INPUT_OBJECT", "name": "songs_order_by" }, { "kind": "INPUT_OBJECT", "name": "IntComparison" }, { "kind": "INPUT_OBJECT", "name": "FloatComparison" }, { "kind": "INPUT_OBJECT", "name": "StringComparison" }, { "kind": "ENUM", "name": "OrderingTerm" }, { "kind": "OBJECT", "name": "Query" }, { "kind": "OBJECT", "name": "Mutation" }, { "kind": "SCALAR", "name": "Boolean" }, { "kind": "SCALAR", "name": "Int" }, { "kind": "SCALAR", "name": "Float" }, { "kind": "SCALAR", "name": "String" }, { "kind": "SCALAR", "name": "ID" }, { "kind": "OBJECT", "name": "__Schema" }, { "kind": "OBJECT", "name": "__Type" }, { "kind": "ENUM", "name": "__TypeKind" }, { "kind": "OBJECT", "name": "__Field" }, { "kind": "OBJECT", "name": "__InputValue" }, { "kind": "OBJECT", "name": "__EnumValue" }, { "kind": "OBJECT", "name": "__Directive" }, { "kind": "ENUM", "name": "__DirectiveLocation" } ] } } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery result `unorderedShouldBe` expected it "returns directives on __schema" $ do let introspectionQuery :: Text = [gql| query UsersTypeName { __schema { directives { name description locations args { name description defaultValue type { ...TypeRef } } } } } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } |] expected = rmSpaces [raw| { "data": { "__schema": { "directives": [ { "name": "skip", "args": [ { "name": "if", "defaultValue": null, "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind":"SCALAR", "name":"Boolean", "ofType":null } }, "description": "Skipped when true." } ], "locations": [ "INLINE_FRAGMENT", "FRAGMENT_SPREAD", "FIELD" ], "description": "Directs the executor to skip this field or fragment when the `if` argument is true." }, { "name": "include", "args": [ { "name": "if", "defaultValue": null, "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind":"SCALAR", "name":"Boolean", "ofType":null } }, "description": "Included when true." } ], "locations": [ "INLINE_FRAGMENT", "FRAGMENT_SPREAD", "FIELD" ], "description": "Directs the executor to include this field or fragment only when the `if` argument is true." } ] } } } |] conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery Ae.encode result `shouldBe` expected it "supports a full introspection query" $ do gqlFile <- makeAbsolute $ testRoot </> "introspection_query.gql" introspectionQuery <- readFile gqlFile jsonFile <- makeAbsolute $ testRoot </> "introspection_result.json" expected <- readFile jsonFile conn <- SS.open dbPath Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery -- Uncomment to write the new file to disk (for easier diffing) -- P.writeFile (testRoot </> "new_introspection_result.json") $ -- P.decodeUtf8 $ -- BL.toStrict $ -- Ae.encode result result `unorderedShouldBe` expected it "doesn't allow writeonly tokens to return data" $ do let dbName = "no-writeonly-return.db" withTestDbConn dbName $ \conn -> do SS.execute_ conn [sql| CREATE TABLE items ( id INTEGER PRIMARY KEY ) |] let query :: Text query = [gql| mutation items { update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) { returning { id } } } |] expected = rmSpaces [raw| { "data": null, "errors": [{ "locations": [{ "column":5, "line":3 }], "message": "Cannot query field \"returning\" on type \"items_mutation_response\"." }] } |] Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf{accessMode = writeOnly} conn fixtureDbId tables Right response <- graphql schema Nothing mempty query Ae.encode response `shouldBe` expected it "doesn't allow insertonly tokens to update data" $ do let dbName = "no-insertonly-return.db" withTestDbConn dbName $ \conn -> do SS.execute_ conn [sql| CREATE TABLE items ( id INTEGER PRIMARY KEY ) |] let query :: Text query = [gql| mutation items { update_items_by_pk(id: 0, set: { id: 0 }) { affected_rows } } |] expected = rmSpaces [raw| { "data": null, "errors": [{ "locations": [{ "column":3, "line":2 }], "message": "Cannot query field \"update_items_by_pk\" on type \"Mutation\"." }] } |] Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf{accessMode = insertOnly} conn fixtureDbId tables Right response <- graphql schema Nothing mempty query Ae.encode response `shouldBe` expected describe "Naming conflicts" $ do it "appends _ at the end of queries to avoid conflicts with table names" $ do let dbName = "by-pk-table-names.db" withTestDbConn dbName $ \conn -> do SS.execute_ conn [sql| CREATE TABLE foo (id INT) |] SS.execute_ conn [sql| CREATE TABLE foo_by_pk (id INT) |] SS.execute_ conn [sql| CREATE TABLE foo_by_pk_ (id INT) |] let introspectionQuery = [gql| query { __schema { queryType { fields { name } } } } |] expected = [raw| { "data": { "__schema": { "queryType": { "fields": [ { "name": "foo" }, { "name": "foo_by_pk" }, { "name": "foo_by_pk_" }, { "name": "foo_by_pk__" }, { "name": "foo_by_pk_by_pk" }, { "name": "foo_by_pk__by_pk" } ] } } } } |] Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery result `unorderedShouldBe` expected it "appends _ at the end of argument names to avoid conflicts" $ do let dbName = "by-pk-arg-names.db" withTestDbConn dbName $ \conn -> do SS.execute_ conn [sql| CREATE TABLE foo ("_set" INT PRIMARY KEY) |] let introspectionQuery = [gql| query { __schema { mutationType { fields { name, args { name } } } } } |] expected = [raw| { "data": { "__schema": { "mutationType": { "fields": [ { "name": "insert_foo", "args": [ { "name": "objects" }, { "name": "on_conflict" } ] }, { "name": "update_foo", "args": [ { "name": "_set" }, { "name": "where" } ] }, { "name": "update_foo_by_pk", "args": [ { "name": "_set" }, { "name": "_set_" } ] }, { "name": "delete_foo", "args": [{ "name": "where" }] }, { "name": "delete_foo_by_pk", "args": [{ "name": "_set" }] } ] } } } } |] Right tables <- getEnrichedTables conn schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables Right result <- graphql schema Nothing mempty introspectionQuery result `unorderedShouldBe` expected