packages feed

generic-persistence-0.7.0.0: test/ExceptionsSpec.hs

module ExceptionsSpec
  ( test,
    spec,
  )
where

import           Control.Exception
import           Database.GP.GenericPersistenceSafe
import           Database.HDBC
import           Database.HDBC.Sqlite3
import           GHC.Generics
import           Test.Hspec

-- `test` is here so that this module can be run from GHCi on its own.  It is
-- not needed for automatic spec discovery.
-- (start up stack repl --test to bring up ghci and have access to all the test functions)
test :: IO ()
test = hspec spec

prepareDB :: IO Conn
prepareDB = do
  conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
  setupTable @Article conn defaultSqliteMapping
  return conn

data Article = Article
  { articleID :: Int,
    title     :: String,
    year      :: Int
  }
  deriving (Generic, Show, Eq)

instance Entity Article where
  autoIncrement = False
  idField = "articleID"

data Bogus = Bogus
  { bogusID    :: Int,
    bogusTitle :: String,
    bogusYear  :: Int
  }
  deriving (Generic, Show, Eq)

instance Entity Bogus where
  tableName = "Article"

  fieldsToColumns :: [(String, String)] -- ommitting the articles field,
  fieldsToColumns =
    -- as this can not be mapped to a single column
    [ ("bogusID", "articleID"),
      ("bogusTitle", "title"),
      ("bogusYear", "year")
    ]

  fromRow :: Conn -> [SqlValue] -> IO Bogus
  fromRow _conn _row = throw $ DatabaseError "can't create bogus"

yearField :: Field
yearField = field "year"

article :: Article
article = Article 1 "The Hitchhiker's Guide to the Galaxy" 1979

expectationSuccess :: IO ()
expectationSuccess = return ()

spec :: Spec
spec = do
  describe "Exception Handling" $ do
    it "detects duplicate inserts" $ do
      conn <- prepareDB
      _ <- insert conn article
      eitherExRes <- insert conn article :: IO (Either PersistenceException Article)
      case eitherExRes of
        Left di@(DuplicateInsert _msg) -> show di `shouldContain` "Entity already exists"
        _ -> expectationFailure "Expected DuplicateInsert exception"
    it "detects duplicate inserts in insertMany" $ do
      conn <- prepareDB
      _ <- insert conn article
      eitherExRes <- insertMany conn [article, article] :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (DuplicateInsert msg) -> msg `shouldContain` "Entity already exists"
        _ -> expectationFailure "Expected DuplicateInsert exception"
    it "returns () for successful upsert" $ do
      conn <- prepareDB
      eitherExRes <- upsert conn article :: IO (Either PersistenceException ())
      case eitherExRes of
        Right () -> expectationSuccess
        _        -> expectationFailure "Expected ()"
    it "detects missing entities in selectById" $ do
      conn <- prepareDB
      eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Article)
      case eitherExRes of
        Left (EntityNotFound msg) -> msg `shouldContain` "not found"
        _ -> expectationFailure "Expected EntityNotFound exception"
    it "detects non unique entities in selectById" $ do
      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
      runRaw conn "CREATE TABLE article (articleID INTEGER, title TEXT, year INTEGER)"
      _ <- insert conn article
      _ <- insert conn article {title = "another title"}
      eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Article)
      case eitherExRes of
        Left (NoUniqueKey msg) -> msg `shouldContain` "More than one Article found for id SqlString \"1\""
        _ -> expectationFailure "Expected DuplicateEntity exception"

    it "detects bogus data in selectById" $ do
      conn <- prepareDB
      _ <- insert conn article
      eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Bogus)
      case eitherExRes of
        Left (DatabaseError msg) -> msg `shouldContain` "can't create bogus"
        Left pe -> expectationFailure $ "Expected DatabaseError exception, got " ++ show pe
        _ -> expectationFailure "Expected DatabaseError exception"

    it "detects missing entities in update" $ do
      conn <- prepareDB
      eitherExRes <- update conn article :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"
        _ -> expectationFailure "Expected EntityNotFound exception"
    it "detects missing entities in delete" $ do
      conn <- prepareDB
      eitherExRes <- delete conn article :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"
        _ -> expectationFailure "Right: Expected EntityNotFound exception"
    it "detects missing entities in deleteById" $ do
      conn <- prepareDB
      eitherExRes <- deleteById @Article conn "1" :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"
        _ -> expectationFailure "Expected EntityNotFound exception"
    it "detects general db issues in deleteById" $ do
      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
      eitherExRes <- deleteById @Article conn "1" :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (DatabaseError msg) -> msg `shouldContain` "SqlError"
        _ -> expectationFailure "Expected DatabaseError exception"
    it "detects general backend issues" $ do
      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
      eitherExRes <- update conn article :: IO (Either PersistenceException ())
      case eitherExRes of
        Left (DatabaseError msg) -> msg `shouldContain` "SqlError"
        _ -> expectationFailure "Expected DatabaseError exception"
    it "has no leaking backend exceptions" $ do
      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
      _ <- update conn article :: IO (Either PersistenceException ())
      _ <- insert conn article :: IO (Either PersistenceException Article)
      _ <- upsert conn article :: IO (Either PersistenceException ())
      _ <- delete conn article :: IO (Either PersistenceException ())
      _ <- selectById conn "1" :: IO (Either PersistenceException Article)
      _ <- select conn allEntries :: IO (Either PersistenceException [Article])
      _ <- select conn (yearField =. "2023") :: IO (Either PersistenceException [Article])
      _ <- insertMany conn [article] :: IO (Either PersistenceException ())
      _ <- updateMany conn [article] :: IO (Either PersistenceException ())
      _ <- deleteMany conn [article] :: IO (Either PersistenceException ())
      expectationSuccess

    it "can find column names" $ do
      let columnName = columnNameFor @Article "articleID"
      columnName `shouldBe` "articleID"

    it "throws an exception when column name is not found" $ do
      let columnName = columnNameFor @Article "unknown"
      print columnName `shouldThrow` errorCall "columnNameFor: Article has no column mapping for unknown"

    it "reports unknown fields" $ do
      let index = fieldIndex @Article "unknown"
      print index `shouldThrow` errorCall "expectJust Field unknown is not present in type Article"

    it "handles duplicate insert exceptions" $ do
      handleDuplicateInsert (toException $ EntityNotFound "24") `shouldBe` DatabaseError "EntityNotFound \"24\""

    it "handles autoincrement edge cases" $ do
      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
      let article' = Article 200 "title" 2023
          expectedRow = [SqlInt64 200, SqlString "title", SqlInt64 2023]
      articleRow <- toRow conn article'
      articleRow `shouldBe` expectedRow
      -- Article is defined with autoIncrement = False
      removeAutoIncIdField @Article articleRow `shouldBe` expectedRow

      -- ArticleWithoutPK is defined with autoIncrement = True, but has no primary key field
      let articleWithoutPK = ArticleWithoutPK "title" 2023
          rowWithoutPK = [SqlString "title", SqlInt64 2023]
      articleRowWithoutPK <- toRow conn articleWithoutPK
      removeAutoIncIdField @ArticleWithoutPK articleRowWithoutPK `shouldBe` rowWithoutPK

      -- ArticleWithPKatEnd is defined with autoIncrement = True and has a primary key field as last column
      let articleWithPKatEnd = ArticleWithPKatEnd "title" 2023 200
          expectedRow2 = [SqlString "title", SqlInt64 2023]
      articleRowWithPKatEnd <- toRow conn articleWithPKatEnd
      removeAutoIncIdField @ArticleWithPKatEnd articleRowWithPKatEnd `shouldBe` expectedRow2

data ArticleWithoutPK = ArticleWithoutPK
  { title1 :: String,
    year1  :: Int
  }
  deriving (Generic, Show, Entity, Eq)

data ArticleWithPKatEnd = ArticleWithPKatEnd
  { title2     :: String,
    year2      :: Int,
    articleID2 :: Int
  }
  deriving (Generic, Show, Eq)

instance Entity ArticleWithPKatEnd where
  autoIncrement = True
  idField = "articleID2"