{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module AgentTests.SQLiteTests where import AgentTests.EqInstances () import Control.Concurrent.Async (concurrently_) import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Exception (SomeException) import Control.Monad (replicateM_) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import Data.ByteArray (ScrubbedBytes) import Data.ByteString.Char8 (ByteString) import Data.List (isInfixOf) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time import Data.Word (Word32) import Database.SQLite.Simple (Only (..)) import qualified Database.SQLite.Simple as SQL import Database.SQLite.Simple.QQ (sql) import SMPClient (testKeyHash) import Simplex.FileTransfer.Client (XFTPChunkSpec (..)) import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol import Simplex.FileTransfer.Types import Simplex.Messaging.Agent.Client () import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.AgentStore import Simplex.Messaging.Agent.Store.Migrations.App (appMigrations) import Simplex.Messaging.Agent.Store.SQLite import Simplex.Messaging.Agent.Store.SQLite.Common (DBStore (..), withTransaction') import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (pattern IKPQOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Protocol (EntityId (..), QueueMode (..), SubscriptionMode (..), pattern VersionSMPC) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Agent.Store.Entity import System.Random import Test.Hspec hiding (fit, it) import UnliftIO.Directory (removeFile) import Util testDB :: String testDB = "tests/tmp/smp-agent.test.db" withStore :: SpecWith DBStore -> Spec withStore = before createStore' . after removeStore withStore2 :: SpecWith (DBStore, DBStore) -> Spec withStore2 = before connect2 . after (removeStore . fst) where connect2 :: IO (DBStore, DBStore) connect2 = do s1@DBStore {dbFilePath} <- createStore' s2 <- connectSQLiteStore dbFilePath "" False DB.TQOff pure (s1, s2) createStore' :: IO DBStore createStore' = createEncryptedStore "" False createEncryptedStore :: ScrubbedBytes -> Bool -> IO DBStore createEncryptedStore key keepKey = do -- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous -- IO operations on multiple similarly named files; error seems to be environment specific r <- randomIO :: IO Word32 Right st <- createDBStore (DBOpts (testDB <> show r) key keepKey True DB.TQOff) appMigrations (MigrationConfig MCError Nothing) withTransaction' st (`SQL.execute_` "INSERT INTO users (user_id) VALUES (1);") pure st removeStore :: DBStore -> IO () removeStore db@DBStore {dbFilePath} = do close db removeFile dbFilePath where close :: DBStore -> IO () close st = mapM_ DB.close =<< tryTakeMVar (dbConnection st) storeTests :: Spec storeTests = do withStore2 $ do describe "stress test" testConcurrentWrites withStore $ do describe "db setup" $ do testCompiledThreadsafe testForeignKeysEnabled describe "db methods" $ do describe "Queue and Connection management" $ do describe "create Rcv connection" $ do testCreateRcvConn testCreateRcvConnRandomId testCreateRcvConnDuplicate describe "createSndConn" $ do testCreateSndConn testCreateSndConnRandomID testCreateSndConnDuplicate describe "getRcvConn" testGetRcvConn describe "deleteConn" $ do testDeleteRcvConn testDeleteSndConn testDeleteDuplexConn describe "setConnUserId" $ do testSetConnUserIdNewConn describe "upgradeRcvConnToDuplex" $ do testUpgradeRcvConnToDuplex describe "upgradeSndConnToDuplex" $ do testUpgradeSndConnToDuplex describe "set Queue status" $ do describe "setRcvQueueStatus" $ do testSetRcvQueueStatus describe "setSndQueueStatus" $ do testSetSndQueueStatus testSetQueueStatusDuplex describe "Msg management" $ do describe "create Msg" $ do testCreateRcvMsg testCreateSndMsg testCreateRcvAndSndMsgs describe "Work items" $ do it "should getPendingQueueMsg" testGetPendingQueueMsg it "should getPendingServerCommand" testGetPendingServerCommand it "should getNextRcvChunkToDownload" testGetNextRcvChunkToDownload it "should getNextRcvFileToDecrypt" testGetNextRcvFileToDecrypt it "should getNextSndFileToPrepare" testGetNextSndFileToPrepare it "should getNextSndChunkToUpload" testGetNextSndChunkToUpload it "should getNextDeletedSndChunkReplica" testGetNextDeletedSndChunkReplica it "should markNtfSubActionNtfFailed_" testMarkNtfSubActionNtfFailed it "should markNtfSubActionSMPFailed_" testMarkNtfSubActionSMPFailed it "should markNtfTokenToDeleteFailed_" testMarkNtfTokenToDeleteFailed describe "open/close store" $ do it "should close and re-open" testCloseReopenStore it "should close and re-open encrypted store" testCloseReopenEncryptedStore it "should close and re-open encrypted store (keep key)" testReopenEncryptedStoreKeepKey testConcurrentWrites :: SpecWith (DBStore, DBStore) testConcurrentWrites = it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do g <- C.newRandom Right (_, rq) <- withTransaction s1 $ \db -> createRcvConn db g cData1 rcvQueue1 SCMInvitation let ConnData {connId} = cData1 concurrently_ (runTest s1 connId rq) (runTest s2 connId rq) where runTest :: DBStore -> ConnId -> RcvQueue -> IO () runTest st connId rq = replicateM_ 100 . withTransaction st $ \db -> do (internalId, internalRcvId, _, _) <- updateRcvIds db connId let rcvMsgData = mkRcvMsgData internalId internalRcvId 0 "0" "hash_dummy" createRcvMsg db connId rq rcvMsgData testCompiledThreadsafe :: SpecWith DBStore testCompiledThreadsafe = it "compiled sqlite library should be threadsafe" . withStoreTransaction $ \db -> do compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[T.Text]] compileOptions `shouldNotContain` [["THREADSAFE=0"]] withStoreTransaction :: (DB.Connection -> IO a) -> DBStore -> IO a withStoreTransaction = flip withTransaction testForeignKeysEnabled :: SpecWith DBStore testForeignKeysEnabled = it "foreign keys should be enabled" . withStoreTransaction $ \db -> do let inconsistentQuery = [sql| INSERT INTO snd_queues ( host, port, snd_id, conn_id, snd_private_key, e2e_dh_secret, status) VALUES ('smp.simplex.im', '5223', '1234', '2345', x'', x'', 'new'); |] DB.execute_ db inconsistentQuery `shouldThrow` (\e -> SQL.sqlError e == SQL.ErrorConstraint) cData1 :: ConnData cData1 = ConnData { userId = 1, connId = "conn1", connAgentVersion = VersionSMPA 1, enableNtfs = True, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport = CR.PQSupportOn } testPrivateAuthKey :: C.APrivateAuthKey testPrivateAuthKey = C.APrivateAuthKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe" testPublicAuthKey :: C.APublicAuthKey testPublicAuthKey = C.APublicAuthKey C.SEd25519 (C.publicKey "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe") testPrivDhKey :: C.PrivateKeyX25519 testPrivDhKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk" testDhSecret :: C.DhSecretX25519 testDhSecret = "01234567890123456789012345678901" smpServer1 :: SMPServer smpServer1 = SMPServer "smp.simplex.im" "5223" testKeyHash rcvQueue1 :: NewRcvQueue rcvQueue1 = RcvQueue { userId = 1, connId = "conn1", server = smpServer1, rcvId = EntityId "1234", rcvPrivateKey = testPrivateAuthKey, rcvDhSecret = testDhSecret, e2ePrivKey = testPrivDhKey, e2eDhSecret = Nothing, sndId = EntityId "2345", queueMode = Just QMMessaging, shortLink = Nothing, clientService = Nothing, status = New, dbQueueId = DBNewEntity, primary = True, dbReplaceQueueId = Nothing, rcvSwchStatus = Nothing, smpClientVersion = VersionSMPC 1, clientNtfCreds = Nothing, deleteErrors = 0 } sndQueue1 :: NewSndQueue sndQueue1 = SndQueue { userId = 1, connId = "conn1", server = smpServer1, sndId = EntityId "3456", queueMode = Just QMMessaging, sndPublicKey = testPublicAuthKey, sndPrivateKey = testPrivateAuthKey, e2ePubKey = Nothing, e2eDhSecret = testDhSecret, status = New, dbQueueId = DBNewEntity, primary = True, dbReplaceQueueId = Nothing, sndSwchStatus = Nothing, smpClientVersion = VersionSMPC 1 } createRcvConn :: DB.Connection -> TVar ChaChaDRG -> ConnData -> NewRcvQueue -> SConnectionMode c -> IO (Either StoreError (ConnId, RcvQueue)) createRcvConn db g cData rq cMode = runExceptT $ do connId <- ExceptT $ createNewConn db g cData cMode rq' <- ExceptT $ updateNewConnRcv db connId rq pure (connId, rq') testCreateRcvConn :: SpecWith DBStore testCreateRcvConn = it "should create RcvConnection and add SndQueue" . withStoreTransaction $ \db -> do g <- C.newRandom Right (connId, rq@RcvQueue {dbQueueId}) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation connId `shouldBe` "conn1" dbQueueId `shouldBe` DBEntityId 1 getConn db "conn1" `shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rq)) Right sq@SndQueue {dbQueueId = dbQueueId'} <- upgradeRcvConnToDuplex db "conn1" sndQueue1 dbQueueId' `shouldBe` DBEntityId 1 getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq] [sq])) testCreateRcvConnRandomId :: SpecWith DBStore testCreateRcvConnRandomId = it "should create RcvConnection and add SndQueue with random ID" . withStoreTransaction $ \db -> do g <- C.newRandom Right (connId, rq) <- createRcvConn db g cData1 {connId = ""} rcvQueue1 SCMInvitation getConn db connId `shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 {connId} rq)) Right sq@SndQueue {dbQueueId = dbQueueId'} <- upgradeRcvConnToDuplex db connId sndQueue1 dbQueueId' `shouldBe` DBEntityId 1 getConn db connId `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 {connId} [rq] [sq])) testCreateRcvConnDuplicate :: SpecWith DBStore testCreateRcvConnDuplicate = it "should throw error on attempt to create duplicate RcvConnection" . withStoreTransaction $ \db -> do g <- C.newRandom _ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation createRcvConn db g cData1 rcvQueue1 SCMInvitation `shouldReturn` Left SEConnDuplicate testCreateSndConn :: SpecWith DBStore testCreateSndConn = it "should create SndConnection and add RcvQueue" . withStoreTransaction $ \db -> do g <- C.newRandom Right (connId, sq@SndQueue {dbQueueId}) <- createSndConn db g cData1 sndQueue1 connId `shouldBe` "conn1" dbQueueId `shouldBe` DBEntityId 1 getConn db "conn1" `shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sq)) Right rq@RcvQueue {dbQueueId = dbQueueId'} <- upgradeSndConnToDuplex db "conn1" rcvQueue1 dbQueueId' `shouldBe` DBEntityId 1 getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq] [sq])) testCreateSndConnRandomID :: SpecWith DBStore testCreateSndConnRandomID = it "should create SndConnection and add RcvQueue with random ID" . withStoreTransaction $ \db -> do g <- C.newRandom Right (connId, sq) <- createSndConn db g cData1 {connId = ""} sndQueue1 getConn db connId `shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 {connId} sq)) Right (rq@RcvQueue {dbQueueId = dbQueueId'}) <- upgradeSndConnToDuplex db connId rcvQueue1 dbQueueId' `shouldBe` DBEntityId 1 getConn db connId `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 {connId} [rq] [sq])) testCreateSndConnDuplicate :: SpecWith DBStore testCreateSndConnDuplicate = it "should throw error on attempt to create duplicate SndConnection" . withStoreTransaction $ \db -> do g <- C.newRandom _ <- createSndConn db g cData1 sndQueue1 createSndConn db g cData1 sndQueue1 `shouldReturn` Left SEConnDuplicate testGetRcvConn :: SpecWith DBStore testGetRcvConn = it "should get connection using rcv queue id and server" . withStoreTransaction $ \db -> do let smpServer = SMPServer "smp.simplex.im" "5223" testKeyHash let recipientId = EntityId "1234" g <- C.newRandom Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation getRcvConn db smpServer recipientId `shouldReturn` Right (rq, SomeConn SCRcv (RcvConnection cData1 rq)) testSetConnUserIdNewConn :: SpecWith DBStore testSetConnUserIdNewConn = it "should set user id for new connection" . withStoreTransaction $ \db -> do g <- C.newRandom Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation newUserId <- createUserRecord db _ <- setConnUserId db 1 connId newUserId connResult <- getConn db connId case connResult of Right (SomeConn SCNew (NewConnection connData)) -> do let ConnData {userId} = connData userId `shouldBe` newUserId _ -> do expectationFailure "Failed to get connection" testDeleteRcvConn :: SpecWith DBStore testDeleteRcvConn = it "should create RcvConnection and delete it" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation getConn db "conn1" `shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rq)) deleteConn db Nothing "conn1" `shouldReturn` Just "conn1" getConn db "conn1" `shouldReturn` Left SEConnNotFound testDeleteSndConn :: SpecWith DBStore testDeleteSndConn = it "should create SndConnection and delete it" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, sq) <- createSndConn db g cData1 sndQueue1 getConn db "conn1" `shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sq)) deleteConn db Nothing "conn1" `shouldReturn` Just "conn1" getConn db "conn1" `shouldReturn` Left SEConnNotFound testDeleteDuplexConn :: SpecWith DBStore testDeleteDuplexConn = it "should create DuplexConnection and delete it" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation Right sq <- upgradeRcvConnToDuplex db "conn1" sndQueue1 getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq] [sq])) deleteConn db Nothing "conn1" `shouldReturn` Just "conn1" getConn db "conn1" `shouldReturn` Left SEConnNotFound testUpgradeRcvConnToDuplex :: SpecWith DBStore testUpgradeRcvConnToDuplex = it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" . withStoreTransaction $ \db -> do g <- C.newRandom _ <- createSndConn db g cData1 sndQueue1 let anotherSndQueue = SndQueue { userId = 1, connId = "conn1", server = SMPServer "smp.simplex.im" "5223" testKeyHash, sndId = EntityId "2345", queueMode = Just QMMessaging, sndPublicKey = testPublicAuthKey, sndPrivateKey = testPrivateAuthKey, e2ePubKey = Nothing, e2eDhSecret = testDhSecret, status = New, dbQueueId = DBNewEntity, sndSwchStatus = Nothing, primary = True, dbReplaceQueueId = Nothing, smpClientVersion = VersionSMPC 1 } upgradeRcvConnToDuplex db "conn1" anotherSndQueue `shouldReturn` Left (SEBadConnType "upgradeRcvConnToDuplex" CSnd) _ <- upgradeSndConnToDuplex db "conn1" rcvQueue1 upgradeRcvConnToDuplex db "conn1" anotherSndQueue `shouldReturn` Left (SEBadConnType "upgradeRcvConnToDuplex" CDuplex) testUpgradeSndConnToDuplex :: SpecWith DBStore testUpgradeSndConnToDuplex = it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" . withStoreTransaction $ \db -> do g <- C.newRandom _ <- createRcvConn db g cData1 rcvQueue1 SCMInvitation let anotherRcvQueue = RcvQueue { userId = 1, connId = "conn1", server = SMPServer "smp.simplex.im" "5223" testKeyHash, rcvId = EntityId "3456", rcvPrivateKey = testPrivateAuthKey, rcvDhSecret = testDhSecret, e2ePrivKey = testPrivDhKey, e2eDhSecret = Nothing, sndId = EntityId "4567", queueMode = Just QMMessaging, shortLink = Nothing, clientService = Nothing, status = New, dbQueueId = DBNewEntity, rcvSwchStatus = Nothing, primary = True, dbReplaceQueueId = Nothing, smpClientVersion = VersionSMPC 1, clientNtfCreds = Nothing, deleteErrors = 0 } upgradeSndConnToDuplex db "conn1" anotherRcvQueue `shouldReturn` Left (SEBadConnType "upgradeSndConnToDuplex" CRcv) _ <- upgradeRcvConnToDuplex db "conn1" sndQueue1 upgradeSndConnToDuplex db "conn1" anotherRcvQueue `shouldReturn` Left (SEBadConnType "upgradeSndConnToDuplex" CDuplex) testSetRcvQueueStatus :: SpecWith DBStore testSetRcvQueueStatus = it "should update status of RcvQueue" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation getConn db "conn1" `shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rq)) setRcvQueueStatus db rq Confirmed `shouldReturn` () getConn db "conn1" `shouldReturn` Right (SomeConn SCRcv (RcvConnection cData1 rq {status = Confirmed})) testSetSndQueueStatus :: SpecWith DBStore testSetSndQueueStatus = it "should update status of SndQueue" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, sq) <- createSndConn db g cData1 sndQueue1 getConn db "conn1" `shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sq)) setSndQueueStatus db sq Confirmed `shouldReturn` () getConn db "conn1" `shouldReturn` Right (SomeConn SCSnd (SndConnection cData1 sq {status = Confirmed})) testSetQueueStatusDuplex :: SpecWith DBStore testSetQueueStatusDuplex = it "should update statuses of RcvQueue and SndQueue in DuplexConnection" . withStoreTransaction $ \db -> do g <- C.newRandom Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation Right sq <- upgradeRcvConnToDuplex db "conn1" sndQueue1 getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq] [sq])) setRcvQueueStatus db rq Secured `shouldReturn` () let rq' = (rq :: RcvQueue) {status = Secured} getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq'] [sq])) setSndQueueStatus db sq Confirmed `shouldReturn` () let sq' = (sq :: SndQueue) {status = Confirmed} getConn db "conn1" `shouldReturn` Right (SomeConn SCDuplex (DuplexConnection cData1 [rq'] [sq'])) hw :: ByteString hw = encodeUtf8 "Hello world!" ts :: UTCTime ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0) mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash = RcvMsgData { internalRcvId, msgMeta = MsgMeta { integrity = MsgOk, recipient = (unId internalId, ts), sndMsgId = externalSndId, broker = (brokerId, ts), pqEncryption = CR.PQEncOn }, msgType = AM_A_MSG_, msgFlags = SMP.noMsgFlags, msgBody = hw, internalHash, externalPrevSndHash = "hash_from_sender", encryptedMsgHash = "encrypted_msg_hash" } testCreateRcvMsg_ :: DB.Connection -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvQueue -> RcvMsgData -> Expectation testCreateRcvMsg_ db expectedPrevSndId expectedPrevHash connId rq rcvMsgData@RcvMsgData {..} = do let MsgMeta {recipient = (internalId, _)} = msgMeta updateRcvIds db connId `shouldReturn` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash) createRcvMsg db connId rq rcvMsgData `shouldReturn` () testCreateRcvMsg :: SpecWith DBStore testCreateRcvMsg = it "should reserve internal ids and create a RcvMsg" $ \st -> do g <- C.newRandom let ConnData {connId} = cData1 Right (_, rq) <- withTransaction st $ \db -> do createRcvConn db g cData1 rcvQueue1 SCMInvitation withTransaction st $ \db -> do testCreateRcvMsg_ db 0 "" connId rq $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy" testCreateRcvMsg_ db 1 "hash_dummy" connId rq $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy" mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData mkSndMsgData internalId internalSndId internalHash = SndMsgData { internalId, internalSndId, internalTs = ts, msgType = AM_A_MSG_, msgFlags = SMP.noMsgFlags, msgBody = hw, pqEncryption = CR.PQEncOn, internalHash, prevMsgHash = internalHash, sndMsgPrepData_ = Nothing } testCreateSndMsg_ :: DB.Connection -> PrevSndMsgHash -> ConnId -> SndQueue -> SndMsgData -> Expectation testCreateSndMsg_ db expectedPrevHash connId sq sndMsgData@SndMsgData {..} = do updateSndIds db connId `shouldReturn` Right (internalId, internalSndId, expectedPrevHash) createSndMsg db connId sndMsgData `shouldReturn` () createSndMsgDelivery db connId sq internalId `shouldReturn` () testCreateSndMsg :: SpecWith DBStore testCreateSndMsg = it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \st -> do g <- C.newRandom let ConnData {connId} = cData1 Right (_, sq) <- withTransaction st $ \db -> do createSndConn db g cData1 sndQueue1 withTransaction st $ \db -> do testCreateSndMsg_ db "" connId sq $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy" testCreateSndMsg_ db "hash_dummy" connId sq $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy" testCreateRcvAndSndMsgs :: SpecWith DBStore testCreateRcvAndSndMsgs = it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \st -> do let ConnData {connId} = cData1 Right (_, rq) <- withTransaction st $ \db -> do g <- C.newRandom createRcvConn db g cData1 rcvQueue1 SCMInvitation withTransaction st $ \db -> do Right sq <- upgradeRcvConnToDuplex db "conn1" sndQueue1 testCreateRcvMsg_ db 0 "" connId rq $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1" testCreateRcvMsg_ db 1 "rcv_hash_1" connId rq $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2" testCreateSndMsg_ db "" connId sq $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1" testCreateRcvMsg_ db 2 "rcv_hash_2" connId rq $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3" testCreateSndMsg_ db "snd_hash_1" connId sq $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2" testCreateSndMsg_ db "snd_hash_2" connId sq $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3" testCloseReopenStore :: IO () testCloseReopenStore = do st <- createStore' hasMigrations st closeDBStore st closeDBStore st errorGettingMigrations st openSQLiteStore st "" False openSQLiteStore st "" False hasMigrations st closeDBStore st errorGettingMigrations st reopenDBStore st hasMigrations st testCloseReopenEncryptedStore :: IO () testCloseReopenEncryptedStore = do let key = "test_key" st <- createEncryptedStore key False hasMigrations st closeDBStore st closeDBStore st errorGettingMigrations st reopenDBStore st `shouldThrow` \(e :: SomeException) -> "reopenDBStore: no key" `isInfixOf` show e openSQLiteStore st key True openSQLiteStore st key True hasMigrations st closeDBStore st errorGettingMigrations st reopenDBStore st hasMigrations st testReopenEncryptedStoreKeepKey :: IO () testReopenEncryptedStoreKeepKey = do let key = "test_key" st <- createEncryptedStore key True hasMigrations st closeDBStore st errorGettingMigrations st reopenDBStore st hasMigrations st getMigrations :: DBStore -> IO Bool getMigrations st = not . null <$> withTransaction st getCurrentMigrations hasMigrations :: DBStore -> Expectation hasMigrations st = getMigrations st `shouldReturn` True errorGettingMigrations :: DBStore -> Expectation errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "ErrorMisuse" `isInfixOf` show e testGetPendingQueueMsg :: DBStore -> Expectation testGetPendingQueueMsg st = do g <- C.newRandom withTransaction st $ \db -> do Right (connId, sq) <- createSndConn db g cData1 {connId = ""} sndQueue1 Right Nothing <- getPendingQueueMsg db connId sq testCreateSndMsg_ db "" connId sq $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy" DB.execute db "UPDATE messages SET msg_type = cast('bad' as blob) WHERE conn_id = ? AND internal_id = ?" (connId, 1 :: Int) testCreateSndMsg_ db "hash_dummy" connId sq $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy" Left e <- getPendingQueueMsg db connId sq show e `shouldContain` "bad AgentMessageType" DB.query_ db "SELECT conn_id, internal_id FROM snd_message_deliveries WHERE failed = 1" `shouldReturn` [(connId, 1 :: Int)] Right (Just (Nothing, PendingMsgData {msgId})) <- getPendingQueueMsg db connId sq msgId `shouldBe` InternalId 2 testGetPendingServerCommand :: DBStore -> Expectation testGetPendingServerCommand st = do g <- C.newRandom withTransaction st $ \db -> do Right Nothing <- getPendingServerCommand db "" Nothing Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation Right () <- createCommand db "1" connId Nothing command corruptCmd db "1" connId Right () <- createCommand db "2" connId Nothing command Left e <- getPendingServerCommand db connId Nothing show e `shouldContain` "bad AgentCmdType" DB.query_ db "SELECT conn_id, corr_id FROM commands WHERE failed = 1" `shouldReturn` [(connId, "1" :: ByteString)] Right (Just PendingCommand {corrId}) <- getPendingServerCommand db connId Nothing corrId `shouldBe` "2" Right _ <- updateNewConnRcv db connId rcvQueue1 Right Nothing <- getPendingServerCommand db connId $ Just smpServer1 Right () <- createCommand db "3" connId (Just smpServer1) command corruptCmd db "3" connId Right () <- createCommand db "4" connId (Just smpServer1) command Left e' <- getPendingServerCommand db connId (Just smpServer1) show e' `shouldContain` "bad AgentCmdType" DB.query_ db "SELECT conn_id, corr_id FROM commands WHERE failed = 1" `shouldReturn` [(connId, "1" :: ByteString), (connId, "3" :: ByteString)] Right (Just PendingCommand {corrId = corrId'}) <- getPendingServerCommand db connId (Just smpServer1) corrId' `shouldBe` "4" where command = AClientCommand $ NEW True (ACM SCMInvitation) IKPQOn SMSubscribe corruptCmd :: DB.Connection -> ByteString -> ConnId -> IO () corruptCmd db corrId connId = DB.execute db "UPDATE commands SET command = cast('bad' as blob) WHERE conn_id = ? AND corr_id = ?" (connId, corrId) xftpServer1 :: SMP.XFTPServer xftpServer1 = SMP.ProtocolServer SMP.SPXFTP "xftp.simplex.im" "5223" testKeyHash rcvFileDescr1 :: FileDescription 'FRecipient rcvFileDescr1 = FileDescription { party = SFRecipient, size = FileSize $ mb 26, digest = FileDigest "abc", key = testFileSbKey, nonce = testFileCbNonce, chunkSize = defaultChunkSize, chunks = [ FileChunk { chunkNo = 1, digest = chunkDigest, chunkSize = defaultChunkSize, replicas = [FileChunkReplica {server = xftpServer1, replicaId, replicaKey = testFileReplicaKey}] } ], redirect = Nothing } where defaultChunkSize = FileSize $ mb 8 replicaId = ChunkReplicaId $ EntityId "abc" chunkDigest = FileDigest "ghi" testFileSbKey :: C.SbKey testFileSbKey = either error id $ strDecode "00n8p1tJq5E-SGnHcYTOrS4A9I07gTA_WFD6MTFFFOY=" testFileCbNonce :: C.CbNonce testFileCbNonce = either error id $ strDecode "dPSF-wrQpDiK_K6sYv0BDBZ9S4dg-jmu" testFileReplicaKey :: C.APrivateAuthKey testFileReplicaKey = C.APrivateAuthKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe" testGetNextRcvChunkToDownload :: DBStore -> Expectation testGetNextRcvChunkToDownload st = do g <- C.newRandom withTransaction st $ \db -> do Right Nothing <- getNextRcvChunkToDownload db xftpServer1 86400 Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True Left e <- getNextRcvChunkToDownload db xftpServer1 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just (RcvFileChunk {rcvFileEntityId}, _, Nothing)) <- getNextRcvChunkToDownload db xftpServer1 86400 rcvFileEntityId `shouldBe` fId2 testGetNextRcvFileToDecrypt :: DBStore -> Expectation testGetNextRcvFileToDecrypt st = do g <- C.newRandom withTransaction st $ \db -> do Right Nothing <- getNextRcvFileToDecrypt db 86400 Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 1" DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 2" Left e <- getNextRcvFileToDecrypt db 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just RcvFile {rcvFileEntityId}) <- getNextRcvFileToDecrypt db 86400 rcvFileEntityId `shouldBe` fId2 testGetNextSndFileToPrepare :: DBStore -> Expectation testGetNextSndFileToPrepare st = do g <- C.newRandom withTransaction st $ \db -> do Right Nothing <- getNextSndFileToPrepare db 86400 Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing DB.execute_ db "UPDATE snd_files SET status = 'new', num_recipients = 'bad' WHERE snd_file_id = 1" Right fId2 <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing DB.execute_ db "UPDATE snd_files SET status = 'new' WHERE snd_file_id = 2" Left e <- getNextSndFileToPrepare db 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just SndFile {sndFileEntityId}) <- getNextSndFileToPrepare db 86400 sndFileEntityId `shouldBe` fId2 newSndChunkReplica1 :: NewSndChunkReplica newSndChunkReplica1 = NewSndChunkReplica { server = xftpServer1, replicaId = ChunkReplicaId $ EntityId "abc", replicaKey = testFileReplicaKey, rcvIdsKeys = [(ChunkReplicaId $ EntityId "abc", testFileReplicaKey)] } testGetNextSndChunkToUpload :: DBStore -> Expectation testGetNextSndChunkToUpload st = do g <- C.newRandom withTransaction st $ \db -> do Right Nothing <- getNextSndChunkToUpload db xftpServer1 86400 -- create file 1 Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing updateSndFileEncrypted db 1 (FileDigest "abc") [(XFTPChunkSpec "filepath" 1 1, FileDigest "ghi")] createSndFileReplica_ db 1 newSndChunkReplica1 DB.execute_ db "UPDATE snd_files SET num_recipients = 'bad' WHERE snd_file_id = 1" -- create file 2 Right fId2 <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing updateSndFileEncrypted db 2 (FileDigest "abc") [(XFTPChunkSpec "filepath" 1 1, FileDigest "ghi")] createSndFileReplica_ db 2 newSndChunkReplica1 Left e <- getNextSndChunkToUpload db xftpServer1 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just SndFileChunk {sndFileEntityId}) <- getNextSndChunkToUpload db xftpServer1 86400 sndFileEntityId `shouldBe` fId2 testGetNextDeletedSndChunkReplica :: DBStore -> Expectation testGetNextDeletedSndChunkReplica st = do withTransaction st $ \db -> do Right Nothing <- getNextDeletedSndChunkReplica db xftpServer1 86400 createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi") DB.execute_ db "UPDATE deleted_snd_chunk_replicas SET delay = 'bad' WHERE deleted_snd_chunk_replica_id = 1" createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi") Left e <- getNextDeletedSndChunkReplica db xftpServer1 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT deleted_snd_chunk_replica_id FROM deleted_snd_chunk_replicas WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just DeletedSndChunkReplica {deletedSndChunkReplicaId}) <- getNextDeletedSndChunkReplica db xftpServer1 86400 deletedSndChunkReplicaId `shouldBe` 2 testMarkNtfSubActionNtfFailed :: DBStore -> Expectation testMarkNtfSubActionNtfFailed st = do withTransaction st $ \db -> do markNtfSubActionNtfFailed_ db "abc" testMarkNtfSubActionSMPFailed :: DBStore -> Expectation testMarkNtfSubActionSMPFailed st = do withTransaction st $ \db -> do markNtfSubActionSMPFailed_ db "abc" testMarkNtfTokenToDeleteFailed :: DBStore -> Expectation testMarkNtfTokenToDeleteFailed st = do withTransaction st $ \db -> do markNtfTokenToDeleteFailed_ db 1