packages feed

festung-0.9.1.1: tests/Festung/Vault/PersistenceSpec.hs

{-# LANGUAGE NamedFieldPuns #-}

module Festung.Vault.PersistenceSpec (spec) where

import Test.Hspec

import Control.Monad.Catch    (MonadMask)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Either
import Data.Word

import Festung.Vault.Persistence
import TestUtils


withVault :: (MonadIO m, MonadMask m) => (VaultHandle -> m a) -> m a
withVault action = withVaultName $ \ vaultName -> do
    vaultHandle <- liftIO $ unwrap <$> openVault vaultName password
    res <- action vaultHandle
    -- FIXME(Antoine): Should this be `finally`
    liftIO $ closeVault vaultHandle
    return res


withDataVault :: (MonadIO m, MonadMask m) => (VaultHandle -> m a) -> m a
withDataVault action = withVault $ \ vault -> do
    liftIO $ do createTable vault
                insertData vault
    action vault

password :: [Word8]
password = [0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]

otherPassword :: [Word8]
otherPassword = [0x66, 0x69, 0x67, 0x6f, 0x20, 0x47, 0x6d, 0x62, 0x48]

isNullQueryResult :: QueryResult -> Bool
isNullQueryResult QueryResult{rows} = null rows

createTable :: VaultHandle -> IO (Either Error QueryResult)
createTable = flip executeQuery $
    unlines [ "CREATE TABLE foo ("
            , "   bar  TEXT,"
            , "   baz  INT,"
            , "   qux  REAL,"
            , "   quux BLOB"
            , ");"
            ]

insertData :: VaultHandle -> IO (Either Error QueryResult)
insertData = flip executeQuery $
    unlines [ "INSERT INTO foo(bar, baz, qux, quux) VALUES"
            , "    ('a', 1, 1.5, x'DEADBEEF'),"
            , "    ('b', 2, 2.6, x'C0C4C01A'),"
            , "    ('c', 3, 3.7, x'DEADD00D')"
            ]

spec :: Spec
spec = do
    describe "openVault" $ do
        it "Creates the vault file if it does not exist" $ withVaultName $ \ vaultName -> do
            vault <- openVault vaultName password
            vault `shouldSatisfy` (not . isLeft)
            closeVault (unwrap vault)

        it "Returns an error if the password is incorrect" $ withVaultName $ \ vaultName -> do
            vault <- openVault vaultName password
            let vault' = unwrap vault
            -- SQLCipher only sets the password when data is written to the database,
            -- this includes CREATE TABLE and INSERTs
            createTable vault'
            closeVault vault'
            vault <- openVault vaultName otherPassword
            vault `shouldSatisfy` isLeft

    describe "executeQuery" $ do
        it "Returns an empty result when creating a table" $ withVault $ \ vault -> do
            result <- unwrap <$> executeQuery vault "CREATE TABLE foo(bar text)"
            result `shouldSatisfy` isNullQueryResult

        it "Returns an empty result when inserting data" $ withVault $ \ vault -> do
            createTable vault
            result <- unwrap <$> insertData vault
            result `shouldSatisfy` isNullQueryResult

        it "Can query data" $ withDataVault $ \ vault -> do
            QueryResult {rows} <- unwrap <$> executeQuery vault "SELECT bar FROM foo ORDER BY 1"
            rows `shouldBe` [ [StringValue "a"]
                            , [StringValue "b"]
                            , [StringValue "c"]
                            ]

        it "Returns headers for empty queries" $ withVault $ \ vault -> do
            createTable vault
            result <- unwrap <$> executeQuery vault "SELECT bar, baz, qux, quux FROM foo"
            rows result `shouldSatisfy` null
            headers result `shouldBe` [ ("bar" , Just "TEXT")
                                      , ("baz" , Just "INT" )
                                      , ("qux" , Just "REAL")
                                      , ("quux", Just "BLOB")
                                      ]


        it "Raises an error when multiple SQL statements are being run" pending

        it "Preserves types from the database" $ withDataVault $ \ vault -> do
            QueryResult {rows} <- unwrap <$> executeQuery vault
                "SELECT bar, baz, qux, quux FROM foo ORDER BY 1 LIMIT 1"
            rows `shouldBe` [[ StringValue "a"
                             , IntValue    1
                             , FloatValue  1.5
                             , BlobValue   [0xDE, 0xAD, 0xBE, 0xEF]
                             ]]

            result <- executeQuery vault $  "INSERT INTO foo(bar, baz, qux, quux) VALUES "
                                         ++ "    (NULL, NULL, NULL, NULL)"
            result `shouldSatisfy` isRight

    describe "executeParameterizedQuery" $ do
        it "Binds parameters to the query" $ withDataVault $ \ vault -> do
            QueryResult {rows} <- unwrap <$> executeParameterizedQuery vault
                "SELECT bar FROM foo WHERE bar = ? ORDER BY 1" [StringValue "a"]
            rows `shouldBe` [ [StringValue "a"] ]

        it "All times of parameters" $ withVault $ \ vault -> do
            createTable vault
            QueryResult {lastRowId} <- unwrap <$> executeParameterizedQuery vault
                "INSERT INTO foo(bar, baz, qux, quux) VALUES (?, ?, ?, ?)"
                [StringValue "hello", IntValue 1337, FloatValue 3.14, NullValue]
            lastRowId `shouldBe` 1

            -- FIXME(Antoine): Support blob
            result <- executeParameterizedQuery vault "SELECT ?" [BlobValue []]
            result `shouldSatisfy` isLeft

        it "Raises an error when the amount of parameters doesn't match" $ withDataVault $ \ vault -> do
            result <- executeParameterizedQuery vault
                "SELECT bar FROM foo WHERE bar IN (?, ?) ORDER BY 1" [StringValue "a"]
            result `shouldSatisfy` isLeft

        it "Binds the right type" $ withDataVault $ \ vault -> do
            executeQuery vault $  "INSERT INTO foo(bar, baz, qux, quux) VALUES "
                               ++ "    ('NULL', 0, 0.0, '')"
            executeQuery vault $  "INSERT INTO foo(bar, baz, qux, quux) VALUES "
                               ++ "    (NULL, NULL, NULL, NULL)"

            QueryResult {rows} <- unwrap <$> executeParameterizedQuery vault
                "SELECT count(*) as c FROM foo WHERE bar is ? or baz is ? or qux is ? or quux is ?"
                (replicate 4 NullValue)

            rows `shouldBe` [[IntValue 1]]