mirror of
https://github.com/Airsequel/AirGQL.git
synced 2025-07-09 11:39:33 +02:00
772 lines
24 KiB
Haskell
772 lines
24 KiB
Haskell
-- Unit and integration tests
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE NoMonomorphismRestriction #-}
|
|
{-# OPTIONS_GHC -Wno-missing-local-signatures #-}
|
|
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
|
|
{-# HLINT ignore "Replace case with maybe" #-}
|
|
|
|
import Protolude (
|
|
Bool (False, True),
|
|
Either (Right),
|
|
FilePath,
|
|
IO,
|
|
Maybe (Just, Nothing),
|
|
($),
|
|
(&),
|
|
(<>),
|
|
)
|
|
import Protolude qualified as P
|
|
|
|
import Data.Aeson (Value (Number))
|
|
import Data.Aeson.KeyMap qualified as KeyMap
|
|
import Data.List qualified as List
|
|
import Data.Text qualified as T
|
|
import Database.SQLite.Simple (
|
|
Query,
|
|
execute_,
|
|
open,
|
|
)
|
|
import Database.SQLite.Simple qualified as SS
|
|
import Database.SQLite.Simple.QQ (sql)
|
|
import Servant.Server (runHandler)
|
|
import System.Directory (createDirectoryIfMissing)
|
|
import System.FilePath ((</>))
|
|
import System.Posix (changeWorkingDirectory, getWorkingDirectory)
|
|
import Test.Hspec (
|
|
SpecWith,
|
|
before_,
|
|
describe,
|
|
it,
|
|
pendingWith,
|
|
shouldBe,
|
|
shouldContain,
|
|
shouldMatchList,
|
|
)
|
|
import Test.Hspec qualified as Hspec
|
|
|
|
import AirGQL.Lib (
|
|
ColumnEntry (
|
|
ColumnEntry,
|
|
column_name,
|
|
column_name_gql,
|
|
datatype,
|
|
datatype_gql,
|
|
dflt_value,
|
|
isGenerated,
|
|
isOmittable,
|
|
isUnique,
|
|
notnull,
|
|
primary_key,
|
|
select_options
|
|
),
|
|
GqlTypeName (GqlTypeName, full, root),
|
|
ObjectType (Table),
|
|
SQLPost (SQLPost),
|
|
TableEntryRaw (
|
|
TableEntryRaw,
|
|
name,
|
|
object_type,
|
|
rootpage,
|
|
tbl_name
|
|
),
|
|
getColumns,
|
|
getTables,
|
|
replaceCaseInsensitive,
|
|
stringToGqlTypeName,
|
|
)
|
|
import AirGQL.Lib qualified
|
|
import AirGQL.Raw (raw)
|
|
import AirGQL.Servant.SqlQuery (sqlQueryPostHandler)
|
|
import AirGQL.Types.PragmaConf qualified as PragmaConf
|
|
import AirGQL.Types.SqlQueryPostResult (
|
|
SqlQueryPostResult (
|
|
affectedTables,
|
|
errors,
|
|
rows
|
|
),
|
|
)
|
|
import AirGQL.Types.Utils (encodeToText)
|
|
import AirGQL.Utils (
|
|
getOrderOfLinkedList,
|
|
removeIfExists,
|
|
withRetryConn,
|
|
)
|
|
import Tests.IntrospectionSpec qualified
|
|
import Tests.MutationSpec qualified
|
|
import Tests.QuerySpec qualified
|
|
import Tests.Utils (
|
|
dbPath,
|
|
fixtureDbId,
|
|
withDataDbConn,
|
|
withTestDbConn,
|
|
)
|
|
|
|
|
|
createUsersTableQuery :: Query
|
|
createUsersTableQuery =
|
|
[sql|
|
|
CREATE TABLE IF NOT EXISTS "users" (
|
|
"name" TEXT,
|
|
"email" TEXT NOT NULL UNIQUE,
|
|
"created_utc" TEXT NOT NULL,
|
|
"number_of_logins" INTEGER,
|
|
"progress" REAL,
|
|
PRIMARY KEY("email")
|
|
);
|
|
|]
|
|
|
|
|
|
createUsersTable :: SS.Connection -> IO ()
|
|
createUsersTable conn =
|
|
execute_ conn createUsersTableQuery
|
|
|
|
|
|
createSongsTableQuery :: Query
|
|
createSongsTableQuery =
|
|
[sql|
|
|
CREATE TABLE IF NOT EXISTS "songs" (
|
|
"name" TEXT NOT NULL,
|
|
"duration_seconds" INTEGER NOT NULL
|
|
);
|
|
|]
|
|
|
|
|
|
createSongsTable :: SS.Connection -> IO ()
|
|
createSongsTable conn =
|
|
execute_ conn createSongsTableQuery
|
|
|
|
|
|
-- prettyPrintSchema :: Schema m -> Text
|
|
-- prettyPrintSchema schema =
|
|
-- let
|
|
-- ObjectType name queryDesc interfaceTypes resolvers = query schema
|
|
-- in ""
|
|
-- <> "Description:\n" <> show (description schema) <> "\n\n"
|
|
-- <> "Query:\n\n"
|
|
-- <> " Name:\n" <> name <> "\n\n"
|
|
-- <> " Description:\n" <> show queryDesc <> "\n\n"
|
|
-- <> " InterfaceTypes:\n" <> show interfaceTypes <> "\n\n"
|
|
-- <> " Resolvers:\n" <> show resolvers <> "\n\n"
|
|
-- <> "Mutation:\n\n" <> show (mutation schema) <> "\n\n"
|
|
-- <> "Subscriptions:\n" <> show (subscription schema) <> "\n\n"
|
|
-- -- "Directives:\n" <> show (directives schema) <> "\n\n" <>
|
|
-- -- "Types:\n" <> show (types schema) <> "\n\n" <>
|
|
-- -- "Interface Implementations:\n" <> show (implementations schema) <> "\n\n"
|
|
-- <> ""
|
|
|
|
testSuite :: SpecWith ()
|
|
testSuite = do
|
|
describe "Utils" $ do
|
|
-- Hspec.fit "pretty prints the schema" $ do
|
|
-- conn <- open $ unpack dbPath
|
|
-- tables <- getTables conn
|
|
-- schema <-
|
|
-- getDerivedSchema defaultSchemaConf conn dbPath tables
|
|
|
|
-- putText $ prettyPrintSchema schema
|
|
|
|
-- True `shouldBe` True
|
|
|
|
it "replaces strings case insensitively" $ do
|
|
let
|
|
results =
|
|
[ replaceCaseInsensitive "hello" "hi" "hello world"
|
|
, replaceCaseInsensitive "hello" "hi" "Hello World"
|
|
, replaceCaseInsensitive "l" "L" "Hello World"
|
|
]
|
|
|
|
results `shouldBe` ["hi world", "hi World", "HeLLo WorLd"]
|
|
|
|
it "loads all tables from database" $ do
|
|
tables <- withRetryConn dbPath $ \conn ->
|
|
getTables conn
|
|
|
|
shouldBe
|
|
tables
|
|
[ TableEntryRaw
|
|
{ name = "users"
|
|
, tbl_name = "users"
|
|
, object_type = Table
|
|
, rootpage = 2
|
|
, AirGQL.Lib.sql =
|
|
"\
|
|
\CREATE TABLE \"users\" (\n\
|
|
\ \"name\" TEXT,\n\
|
|
\ \"email\" TEXT NOT NULL UNIQUE,\n\
|
|
\ \"created_utc\" TEXT NOT NULL,\n\
|
|
\ \"number_of_logins\" INTEGER,\n\
|
|
\ \"progress\" REAL,\n\
|
|
\ PRIMARY KEY(\"email\")\n\
|
|
\ )"
|
|
}
|
|
, TableEntryRaw
|
|
{ name = "songs"
|
|
, tbl_name = "songs"
|
|
, object_type = Table
|
|
, rootpage = 4
|
|
, AirGQL.Lib.sql =
|
|
"\
|
|
\CREATE TABLE \"songs\" (\n\
|
|
\ \"name\" TEXT NOT NULL,\n\
|
|
\ \"duration_seconds\" INTEGER NOT NULL\n\
|
|
\ )"
|
|
}
|
|
]
|
|
|
|
describe "getColumns" $ do
|
|
it "loads all columns from users table" $ do
|
|
tableColumns <- withRetryConn dbPath $ \conn ->
|
|
getColumns fixtureDbId conn "users"
|
|
|
|
let
|
|
columnsExpected =
|
|
[ ColumnEntry
|
|
{ column_name = "rowid"
|
|
, column_name_gql = "rowid"
|
|
, datatype = "INTEGER"
|
|
, datatype_gql = Just $ stringToGqlTypeName "Int"
|
|
, select_options = Nothing
|
|
, notnull = True
|
|
, isUnique = True
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = True
|
|
, isRowid = True
|
|
}
|
|
, ColumnEntry
|
|
{ column_name = "name"
|
|
, column_name_gql = "name"
|
|
, datatype = "TEXT"
|
|
, datatype_gql = Just $ stringToGqlTypeName "String"
|
|
, select_options = Nothing
|
|
, notnull = False
|
|
, isUnique = False
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
, ColumnEntry
|
|
{ column_name = "email"
|
|
, column_name_gql = "email"
|
|
, datatype = "TEXT"
|
|
, datatype_gql = Just $ stringToGqlTypeName "String"
|
|
, select_options = Nothing
|
|
, notnull = True
|
|
, isUnique = True
|
|
, isOmittable = False
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = True
|
|
, isRowid = False
|
|
}
|
|
, ColumnEntry
|
|
{ column_name = "created_utc"
|
|
, column_name_gql = "created_utc"
|
|
, datatype = "TEXT"
|
|
, datatype_gql = Just $ stringToGqlTypeName "String"
|
|
, select_options = Nothing
|
|
, notnull = True
|
|
, isUnique = False
|
|
, isOmittable = False
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
, ColumnEntry
|
|
{ column_name = "number_of_logins"
|
|
, column_name_gql = "number_of_logins"
|
|
, datatype = "INTEGER"
|
|
, datatype_gql = Just $ stringToGqlTypeName "Int"
|
|
, select_options = Nothing
|
|
, notnull = False
|
|
, isUnique = False
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
, ColumnEntry
|
|
{ column_name = "progress"
|
|
, column_name_gql = "progress"
|
|
, datatype = "REAL"
|
|
, datatype_gql = Just $ stringToGqlTypeName "Float"
|
|
, select_options = Nothing
|
|
, notnull = False
|
|
, isUnique = False
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
]
|
|
|
|
tableColumns `shouldBe` columnsExpected
|
|
|
|
it "loads a nullable single-select column" $ do
|
|
let dbName = "creates_nullable_single-select.db"
|
|
withTestDbConn dbName $ \conn -> do
|
|
execute_
|
|
conn
|
|
[sql|
|
|
CREATE TABLE checks (
|
|
color TEXT CHECK (color IN ('red', 'green', 'blue'))
|
|
)
|
|
|]
|
|
|
|
tableColumns <- getColumns dbName conn "checks"
|
|
|
|
let
|
|
columnExpected =
|
|
ColumnEntry
|
|
{ column_name = "color"
|
|
, column_name_gql = "color"
|
|
, datatype = "TEXT"
|
|
, datatype_gql =
|
|
Just $
|
|
GqlTypeName
|
|
{ full = "checks_color_String"
|
|
, root = "String"
|
|
}
|
|
, select_options = Just ["red", "green", "blue"]
|
|
, notnull = False
|
|
, isUnique = False
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
|
|
P.lastMay tableColumns `shouldBe` Just columnExpected
|
|
|
|
it "loads a non-null single-select column" $ do
|
|
let dbName = "creates_non-null_single-select.db"
|
|
withTestDbConn dbName $ \conn -> do
|
|
execute_
|
|
conn
|
|
[sql|
|
|
CREATE TABLE checks (
|
|
color TEXT CHECK (color IN ('red', 'green', 'blue')) NOT NULL
|
|
)
|
|
|]
|
|
|
|
tableColumns <- getColumns dbName conn "checks"
|
|
|
|
let
|
|
columnExpected =
|
|
ColumnEntry
|
|
{ column_name = "color"
|
|
, column_name_gql = "color"
|
|
, datatype = "TEXT"
|
|
, datatype_gql =
|
|
Just $
|
|
GqlTypeName
|
|
{ full = "checks_color_String"
|
|
, root = "String"
|
|
}
|
|
, select_options = Just ["red", "green", "blue"]
|
|
, notnull = True
|
|
, isUnique = False
|
|
, isOmittable = False
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
|
|
P.lastMay tableColumns `shouldBe` Just columnExpected
|
|
|
|
it "coerces a multi-type single-select column to text" $ do
|
|
let dbName = "multi-type_single-select.db"
|
|
withTestDbConn dbName $ \conn -> do
|
|
execute_
|
|
conn
|
|
[sql|
|
|
CREATE TABLE checks (
|
|
value TEXT CHECK (value IN (1, 2.2, 'three'))
|
|
)
|
|
|]
|
|
|
|
tableColumns <- getColumns dbName conn "checks"
|
|
|
|
let
|
|
columnExpected =
|
|
ColumnEntry
|
|
{ column_name = "value"
|
|
, column_name_gql = "value"
|
|
, datatype = "TEXT"
|
|
, datatype_gql =
|
|
Just $
|
|
GqlTypeName
|
|
{ full = "checks_value_String"
|
|
, root = "String"
|
|
}
|
|
, select_options = Just ["1", "2.2", "three"]
|
|
, notnull = False
|
|
, isUnique = False
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = False
|
|
, isRowid = False
|
|
}
|
|
|
|
P.lastMay tableColumns `shouldBe` Just columnExpected
|
|
|
|
it "marks integer primary keys as omittable" $ do
|
|
let dbName = "integer-omittable-primary-key.db"
|
|
withTestDbConn dbName $ \conn -> do
|
|
execute_
|
|
conn
|
|
[sql|
|
|
CREATE TABLE items (
|
|
id INTEGER PRIMARY KEY
|
|
)
|
|
|]
|
|
|
|
tableColumns <- getColumns dbName conn "items"
|
|
|
|
let
|
|
columnExpected =
|
|
ColumnEntry
|
|
{ column_name = "id"
|
|
, column_name_gql = "id"
|
|
, datatype = "INTEGER"
|
|
, datatype_gql = Just $ stringToGqlTypeName "Int"
|
|
, select_options = Nothing
|
|
, notnull = True
|
|
, isUnique = True
|
|
, isOmittable = True
|
|
, isGenerated = False
|
|
, isReference = False
|
|
, dflt_value = Nothing
|
|
, primary_key = True
|
|
, isRowid = False
|
|
}
|
|
|
|
P.lastMay tableColumns `shouldBe` Just columnExpected
|
|
|
|
it "correctly parses generated columns" $ do
|
|
let dbName = "generated-columns.db"
|
|
withTestDbConn dbName $ \conn -> do
|
|
execute_
|
|
conn
|
|
[sql|
|
|
CREATE TABLE "generated_columns" (
|
|
"start" REAL NOT NULL,
|
|
"end" REAL NOT NULL,
|
|
"distance_km" REAL GENERATED ALWAYS AS ("end" - "start") STORED,
|
|
"distance_miles" REAL GENERATED ALWAYS AS (distance_km * 1.6) VIRTUAL
|
|
)
|
|
|]
|
|
|
|
tableColumns <- getColumns dbName conn "generated_columns"
|
|
|
|
let (generated, non_generated) =
|
|
-- => [ColumnDef]
|
|
tableColumns
|
|
-- => ([ColumnDef], [ColumnDef])
|
|
& List.partition (\c -> c.isGenerated)
|
|
-- => (Text, Text)
|
|
& P.bimap
|
|
(P.fmap column_name)
|
|
(P.fmap column_name)
|
|
|
|
-- We use `shouldMatchList` because the order does not matter
|
|
non_generated `shouldMatchList` ["start", "end", "rowid"]
|
|
generated `shouldMatchList` ["distance_km", "distance_miles"]
|
|
|
|
it "sorts the tables from the metadata table" $ do
|
|
getOrderOfLinkedList [("a", Nothing), ("b", Just "a"), ("c", Just "b")]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("a", Nothing), ("c", Just "b"), ("b", Just "a")]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("b", Just "a"), ("c", Just "b"), ("a", Nothing)]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("a", Just "x"), ("c", Just "b"), ("b", Just "a")]
|
|
`shouldBe` ["x", "a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("c", Just "b"), ("b", Just "a"), ("a", Just "x")]
|
|
`shouldBe` ["x", "a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("a", Nothing), ("b", Just "a"), ("c", Nothing)]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("a", Nothing), ("b", Nothing), ("c", Nothing)]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
getOrderOfLinkedList [("a", Nothing), ("b", Just "a"), ("d", Just "c")]
|
|
`shouldBe` ["a", "b", "c", "d"]
|
|
|
|
-- Nothing must always be first
|
|
getOrderOfLinkedList [("c", Just "b"), ("d", Just "c"), ("a", Nothing)]
|
|
`shouldBe` ["a", "b", "c", "d"]
|
|
|
|
-- Should *not* error out on cycles
|
|
getOrderOfLinkedList [("c", Just "a"), ("b", Just "a"), ("a", Just "b")]
|
|
`shouldBe` ["a", "b", "c"]
|
|
|
|
describe "REST API" $ do
|
|
describe "SQL" $ do
|
|
it "supports retrieving the schema" $ do
|
|
-- let query = "CREATE TABLE users (name TEXT, email TEXT);"
|
|
-- let dbPost = DatabasePost "test-db" (Just "test-team") "" (Just query)
|
|
|
|
-- Right schemaResult <-
|
|
-- runHandler $
|
|
-- apiDatabaseSchemaGetHandler ctx "TODO"
|
|
|
|
-- T.strip schemaResult `shouldBe` query
|
|
pendingWith "Create database first"
|
|
|
|
it "supports executing an SQL query" $ do
|
|
let dbId = "api-sql-query"
|
|
withDataDbConn dbId $ \_ -> do
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = "SELECT TRUE, 123, 'test'"}
|
|
|
|
result.rows
|
|
`shouldBe` [ KeyMap.fromList
|
|
[ ("TRUE", Number 1)
|
|
, ("123", Number 123)
|
|
, ("'test'", "test")
|
|
]
|
|
]
|
|
|
|
result.affectedTables `shouldMatchList` []
|
|
|
|
it "return columns in the requested order" $ do
|
|
-- Even though JSON objects are unordered by definition,
|
|
-- the fields (columns) must be returned in the requested order
|
|
-- as Elm relies on it for decoding.
|
|
|
|
let dbId = "api-sql-col-order"
|
|
withDataDbConn dbId $ \_ -> do
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = "SELECT 0 AS b, 0 AS a"}
|
|
|
|
(result & encodeToText & T.unpack)
|
|
`shouldContain` "[{\"b\":0,\"a\":0}]"
|
|
|
|
result.affectedTables `shouldMatchList` []
|
|
|
|
it "supports using math functions" $ do
|
|
let dbId = "api-sql-math-query"
|
|
withDataDbConn dbId $ \_ -> do
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = "SELECT abs(floor(cos(2 * pi() / 3)))"}
|
|
|
|
result.rows
|
|
`shouldBe` [ KeyMap.singleton
|
|
"abs(floor(cos(2 * pi() / 3)))"
|
|
(Number 1.0)
|
|
]
|
|
|
|
result.affectedTables `shouldMatchList` []
|
|
|
|
-- The following few tests verify that different operations return
|
|
-- the proper set of affected tables. This query creates a completely
|
|
-- unrelated table just to ensure it is not getting returned by the
|
|
-- methods tested below
|
|
let createUnrelatedTable = do
|
|
[sql|
|
|
CREATE TABLE IF NOT EXISTS todos (
|
|
title TEXT,
|
|
content TEXT,
|
|
done INTEGER
|
|
)
|
|
|]
|
|
|
|
-- This query creates a table commonly used by the following few tests.
|
|
let createNotesTable = do
|
|
[sql|
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
content TEXT
|
|
)
|
|
|]
|
|
|
|
it "should not allow rowid references" $ do
|
|
let dbId = "api-sql-rowid-references"
|
|
let query = "CREATE TABLE foo ( bar TEXT references goo(rowid) )"
|
|
withDataDbConn dbId $ \_ -> do
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = query}
|
|
|
|
let
|
|
expectedMessage =
|
|
"Column 'bar' references the rowid column of table 'goo'.\n"
|
|
<> "This is not supported by SQLite:\n"
|
|
<> "https://www.sqlite.org/foreignkeys.html"
|
|
|
|
result.errors `shouldBe` [expectedMessage]
|
|
|
|
it "should error out on `without rowid` table creation" $ do
|
|
let dbId = "api-sql-without-rowid"
|
|
let query = "CREATE TABLE foo (bar INTEGER PRIMARY KEY) WITHOUT ROWID"
|
|
withDataDbConn dbId $ \_ -> do
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = query}
|
|
|
|
let
|
|
expectedMessage =
|
|
"Table 'foo' does not have a rowid column. "
|
|
<> "Such tables are not currently supported by Airsequel."
|
|
|
|
result.errors `shouldBe` [expectedMessage]
|
|
|
|
it "should return no affected tables on a simple select" $ do
|
|
let dbId = "api-sql-simple-select"
|
|
withDataDbConn dbId $ \conn -> do
|
|
execute_ conn createUnrelatedTable
|
|
execute_ conn createNotesTable
|
|
|
|
execute_
|
|
conn
|
|
[sql|
|
|
INSERT INTO notes VALUES ("hello")
|
|
|]
|
|
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = "SELECT * from notes"}
|
|
|
|
result.rows
|
|
`shouldBe` [KeyMap.singleton "content" "hello"]
|
|
|
|
result.affectedTables `shouldMatchList` []
|
|
|
|
it "should return the correct affected tables when creating tables" $ do
|
|
let dbId = "api-sql-simple-create"
|
|
withDataDbConn dbId $ \conn -> do
|
|
execute_ conn createUnrelatedTable
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost
|
|
{ query = SS.fromQuery createNotesTable
|
|
}
|
|
|
|
result.affectedTables
|
|
`shouldMatchList` ["notes"]
|
|
|
|
it "should return the correct affected tables when dropping tables" $ do
|
|
let dbId = "api-sql-simple-drop"
|
|
withDataDbConn dbId $ \conn -> do
|
|
execute_ conn createUnrelatedTable
|
|
execute_ conn createNotesTable
|
|
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost{query = "DROP TABLE notes"}
|
|
|
|
result.affectedTables `shouldMatchList` ["notes"]
|
|
|
|
it "should return the correct affected tables when editing tables" $ do
|
|
let dbId = "api-sql-add-column"
|
|
withDataDbConn dbId $ \conn -> do
|
|
execute_ conn createUnrelatedTable
|
|
execute_ conn createNotesTable
|
|
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost
|
|
{ query =
|
|
"ALTER TABLE notes ADD COLUMN title TEXT"
|
|
}
|
|
|
|
result.affectedTables `shouldMatchList` ["notes"]
|
|
|
|
it "should mark every table as affected when changes are detected" $ do
|
|
let dbId = "api-sql-insert-values"
|
|
withDataDbConn dbId $ \conn -> do
|
|
execute_ conn createUnrelatedTable
|
|
execute_ conn createNotesTable
|
|
|
|
Right result <-
|
|
runHandler $
|
|
sqlQueryPostHandler
|
|
PragmaConf.defaultConf
|
|
("_TEST_" <> dbId)
|
|
SQLPost
|
|
{ query =
|
|
[raw| INSERT INTO "notes" VALUES ("hello") |]
|
|
}
|
|
|
|
result.affectedTables `shouldMatchList` ["notes", "todos"]
|
|
|
|
describe "Query" Tests.QuerySpec.main
|
|
describe "Mutation" Tests.MutationSpec.main
|
|
describe "Introspection" Tests.IntrospectionSpec.main
|
|
|
|
|
|
deleteDbEntries :: FilePath -> IO ()
|
|
deleteDbEntries databasePath = do
|
|
conn <- open databasePath
|
|
execute_ conn "delete from users"
|
|
execute_ conn "delete from songs"
|
|
|
|
|
|
main :: IO ()
|
|
main = do
|
|
cwd <- getWorkingDirectory
|
|
createDirectoryIfMissing True (".." </> "data" </> "TEST" </> "data")
|
|
changeWorkingDirectory (cwd </> ".." </> "data" </> "TEST")
|
|
|
|
removeIfExists dbPath
|
|
|
|
withRetryConn dbPath $ \conn -> do
|
|
createUsersTable conn
|
|
createSongsTable conn
|
|
|
|
Hspec.hspec $ before_ (deleteDbEntries dbPath) testSuite
|