core: add more multi send api tests (#4750)

This commit is contained in:
spaced4ndy 2024-08-23 21:05:37 +04:00 committed by GitHub
parent 7b48c59f9f
commit bcd50019be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 611 additions and 34 deletions

View file

@ -2906,6 +2906,7 @@ processChatCommand' vr = \case
(msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ (msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_
msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers
let itemsData = prepareSndItemsData msgs_ cmrs ciFiles_ quotedItems_ let itemsData = prepareSndItemsData msgs_ cmrs ciFiles_ quotedItems_
when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch"
(errs, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live (errs, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live
unless (null errs) $ toView $ CRChatErrors (Just user) errs unless (null errs) $ toView $ CRChatErrors (Just user) errs
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
@ -2969,6 +2970,7 @@ processChatCommand' vr = \case
(msgs_, gsr) <- sendGroupMessages user gInfo ms $ L.map XMsgNew msgContainers (msgs_, gsr) <- sendGroupMessages user gInfo ms $ L.map XMsgNew msgContainers
let itemsData = prepareSndItemsData (L.toList msgs_) cmrs ciFiles_ quotedItems_ let itemsData = prepareSndItemsData (L.toList msgs_) cmrs ciFiles_ quotedItems_
cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live
when (length itemsData /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch"
createMemberSndStatuses cis_ msgs_ gsr createMemberSndStatuses cis_ msgs_ gsr
let (errs, cis) = partitionEithers cis_ let (errs, cis) = partitionEithers cis_
unless (null errs) $ toView $ CRChatErrors (Just user) errs unless (null errs) $ toView $ CRChatErrors (Just user) errs
@ -7038,6 +7040,7 @@ batchSendConnMessagesB _user conn msgFlags msgs_ = do
delivered <- deliverMessagesB msgReqs delivered <- deliverMessagesB msgReqs
let msgs' = concat $ L.zipWith flattenMsgs batched' delivered let msgs' = concat $ L.zipWith flattenMsgs batched' delivered
pqEnc = findLastPQEnc delivered pqEnc = findLastPQEnc delivered
when (length msgs' /= length msgs_) $ logError "batchSendConnMessagesB: msgs_ and msgs' length mismatch"
pure (msgs', pqEnc) pure (msgs', pqEnc)
Nothing -> pure ([], Nothing) Nothing -> pure ([], Nothing)
where where
@ -7190,6 +7193,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do
-- Save as pending for toPending members -- Save as pending for toPending members
let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending
stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs) stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs)
when (length stored /= length pendingMemIds) $ logError "sendGroupMessages_: pendingMemIds and stored length mismatch"
-- Zip for easier access to results -- Zip for easier access to results
let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, _, msgIds) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, _, msgIds) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered
pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored

View file

@ -336,6 +336,9 @@ aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci
aChatItemTs :: AChatItem -> UTCTime aChatItemTs :: AChatItem -> UTCTime
aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci
aChatItemDir :: AChatItem -> MsgDirection
aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir
updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d
updateFileStatus ci@ChatItem {file} status = case file of updateFileStatus ci@ChatItem {file} status = case file of
Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}}

View file

@ -2,6 +2,7 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PatternSynonyms #-}
@ -120,10 +121,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItems u chatItems -> CRNewChatItems u chatItems
concatMap | length chatItems > 20 ->
(\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) if
chatItems | all (\aci -> aChatItemDir aci == MDRcv) chatItems -> ttyUser u [sShow (length chatItems) <> " new messages"]
| all (\aci -> aChatItemDir aci == MDSnd) chatItems -> ttyUser u [sShow (length chatItems) <> " messages sent"]
| otherwise -> ttyUser u [sShow (length chatItems) <> " new messages created"]
| otherwise ->
concatMap
(\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item)
chatItems
CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]

View file

@ -17,6 +17,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (intercalate) import Data.List (intercalate)
import qualified Data.Text as T import qualified Data.Text as T
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.AppSettings (defaultAppSettings) import Simplex.Chat.AppSettings (defaultAppSettings)
import qualified Simplex.Chat.AppSettings as AS import qualified Simplex.Chat.AppSettings as AS
import Simplex.Chat.Call import Simplex.Chat.Call
@ -25,6 +26,7 @@ import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat) import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Version import Simplex.Messaging.Version
@ -52,7 +54,11 @@ chatDirectTests = do
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage it "should send multiline message" testMultilineMessage
it "send large message" testLargeMessage it "send large message" testLargeMessage
describe "batch send messages" $ do
it "send multiple messages api" testSendMulti it "send multiple messages api" testSendMulti
it "send multiple timed messages" testSendMultiTimed
it "send multiple messages, including quote" testSendMultiWithQuote
it "send multiple messages (many chat batches)" testSendMultiManyBatches
describe "duplicate contacts" $ do describe "duplicate contacts" $ do
it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
@ -716,22 +722,27 @@ testDirectMessageDeleteMultipleManyBatches =
\alice bob -> do \alice bob -> do
connectUsers alice bob connectUsers alice bob
alice #> "@bob message 0" msgIdZero <- lastItemId alice
bob <# "alice> message 0"
msgIdFirst <- lastItemId alice
forM_ [(1 :: Int) .. 300] $ \i -> do let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
alice #> ("@bob message " <> show i) cms = intercalate ", " (map cm [1 .. 300 :: Int])
bob <# ("alice> message " <> show i)
alice `send` ("/_send @2 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
msgIdLast <- lastItemId alice msgIdLast <- lastItemId alice
let mIdFirst = read msgIdFirst :: Int forM_ [(1 :: Int) .. 300] $ \i -> do
bob <# ("alice> message " <> show i)
let mIdFirst = (read msgIdZero :: Int) + 1
mIdLast = read msgIdLast :: Int mIdLast = read msgIdLast :: Int
deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) deleteIds = intercalate "," (map show [mIdFirst .. mIdLast])
alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast") alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast")
_ <- getTermLine alice _ <- getTermLine alice
alice <## "301 messages deleted" alice <## "300 messages deleted"
forM_ [(0 :: Int) .. 300] $ \i -> do forM_ [(1 :: Int) .. 300] $ \i -> do
bob <# ("alice> [marked deleted] message " <> show i) bob <# ("alice> [marked deleted] message " <> show i)
testDirectLiveMessage :: HasCallStack => FilePath -> IO () testDirectLiveMessage :: HasCallStack => FilePath -> IO ()
@ -852,6 +863,100 @@ testSendMulti =
bob <# "alice> test 1" bob <# "alice> test 1"
bob <# "alice> test 2" bob <# "alice> test 2"
testSendMultiTimed :: HasCallStack => FilePath -> IO ()
testSendMultiTimed =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/_send @2 ttl=1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "@bob test 1"
alice <# "@bob test 2"
bob <# "alice> test 1"
bob <# "alice> test 2"
alice
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
bob
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
testSendMultiWithQuote :: HasCallStack => FilePath -> IO ()
testSendMultiWithQuote =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice #> "@bob hello"
bob <# "alice> hello"
msgId1 <- lastItemId alice
threadDelay 1000000
bob #> "@alice hi"
alice <# "bob> hi"
msgId2 <- lastItemId alice
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message 1\"}}"
cm2 = "{\"quotedItemId\": " <> msgId1 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 2\"}}"
cm3 = "{\"quotedItemId\": " <> msgId2 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 3\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> ", " <> cm3 <> "]")
alice <## "bad chat command: invalid multi send: live and more than one quote not supported"
alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> "]")
alice <# "@bob message 1"
alice <# "@bob >> hello"
alice <## " message 2"
bob <# "alice> message 1"
bob <# "alice> >> hello"
bob <## " message 2"
alice ##> ("/_send @2 json [" <> cm3 <> ", " <> cm1 <> "]")
alice <# "@bob > hi"
alice <## " message 3"
alice <# "@bob message 1"
bob <# "alice> > hi"
bob <## " message 3"
bob <# "alice> message 1"
testSendMultiManyBatches :: HasCallStack => FilePath -> IO ()
testSendMultiManyBatches =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
threadDelay 1000000
msgIdAlice <- lastItemId alice
msgIdBob <- lastItemId bob
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send @2 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i ->
bob <# ("alice> message " <> show i)
aliceItemsCount <- withCCTransaction alice $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]]
aliceItemsCount `shouldBe` [[300]]
bobItemsCount <- withCCTransaction bob $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]]
testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers :: HasCallStack => FilePath -> IO ()
testGetSetSMPServers = testGetSetSMPServers =
testChat2 aliceProfile bobProfile $ testChat2 aliceProfile bobProfile $

View file

@ -36,6 +36,9 @@ chatFileTests = do
it "send and receive image with text and quote" testSendImageWithTextAndQuote it "send and receive image with text and quote" testSendImageWithTextAndQuote
it "send and receive image to group" testGroupSendImage it "send and receive image to group" testGroupSendImage
it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
describe "batch send messages with files" $ do
it "with files folder: send multiple files to contact" testSendMultiFilesDirect
it "with files folder: send multiple files to group" testSendMultiFilesGroup
describe "file transfer over XFTP" $ do describe "file transfer over XFTP" $ do
it "round file description count" $ const testXFTPRoundFDCount it "round file description count" $ const testXFTPRoundFDCount
it "send and receive file" testXFTPFileTransfer it "send and receive file" testXFTPFileTransfer
@ -406,6 +409,166 @@ testGroupSendImageWithTextAndQuote =
cath #$> ("/_get chat #1 count=2", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")]) cath #$> ("/_get chat #1 count=2", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")])
cath @@@ [("#team", "hey bob"), ("@alice", "received invitation to join group team as admin")] cath @@@ [("#team", "hey bob"), ("@alice", "received invitation to join group team as admin")]
testSendMultiFilesDirect :: HasCallStack => FilePath -> IO ()
testSendMultiFilesDirect =
testChat2 aliceProfile bobProfile $ \alice bob -> do
withXFTPServer $ do
connectUsers alice bob
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok")
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "@bob message without file"
alice <# "@bob sending file 1"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "@bob sending file 2"
alice <# "/f @bob test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "alice> message without file"
bob <# "alice> sending file 1"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "alice> sending file 2"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob"
alice <## "completed uploading file 2 (test.pdf) for bob"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2 `shouldBe` src2
alice #$> ("/_get chat @2 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")])
bob #$> ("/_get chat @2 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
testSendMultiFilesGroup :: HasCallStack => FilePath -> IO ()
testSendMultiFilesGroup =
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
withXFTPServer $ do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok")
cath #$> ("/_files_folder ./tests/tmp/cath_app_files", id, "ok")
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send #1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "#team message without file"
alice <# "#team sending file 1"
alice <# "/f #team test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "#team sending file 2"
alice <# "/f #team test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "#team alice> message without file"
bob <# "#team alice> sending file 1"
bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "#team alice> sending file 2"
bob <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
cath <# "#team alice> message without file"
cath <# "#team alice> sending file 1"
cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "#team alice> sending file 2"
cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for #team"
alice <## "completed uploading file 2 (test.pdf) for #team"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
cath ##> "/fr 1"
cath
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
cath <## "completed receiving file 1 (test.jpg) from alice"
cath ##> "/fr 2"
cath
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
cath <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1_1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1_2 <- B.readFile "./tests/tmp/cath_app_files/test.jpg"
dest1_1 `shouldBe` src1
dest1_2 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2_1 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2_2 <- B.readFile "./tests/tmp/cath_app_files/test.pdf"
dest2_1 `shouldBe` src2
dest2_2 `shouldBe` src2
alice #$> ("/_get chat #1 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")])
bob #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
cath #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")])
testXFTPRoundFDCount :: Expectation testXFTPRoundFDCount :: Expectation
testXFTPRoundFDCount = do testXFTPRoundFDCount = do
roundedFDCount (-100) `shouldBe` 4 roundedFDCount (-100) `shouldBe` 4

View file

@ -35,6 +35,8 @@ chatForwardTests = do
it "with relative paths: from notes to group" testForwardFileNotesToGroup it "with relative paths: from notes to group" testForwardFileNotesToGroup
describe "multi forward api" $ do describe "multi forward api" $ do
it "from contact to contact" testForwardContactToContactMulti it "from contact to contact" testForwardContactToContactMulti
it "from group to group" testForwardGroupToGroupMulti
it "with relative paths: multiple files from contact to contact" testMultiForwardFiles
testForwardContactToContact :: HasCallStack => FilePath -> IO () testForwardContactToContact :: HasCallStack => FilePath -> IO ()
testForwardContactToContact = testForwardContactToContact =
@ -620,3 +622,188 @@ testForwardContactToContactMulti =
cath <## " hi" cath <## " hi"
cath <# "alice> -> forwarded" cath <# "alice> -> forwarded"
cath <## " hey" cath <## " hey"
testForwardGroupToGroupMulti :: HasCallStack => FilePath -> IO ()
testForwardGroupToGroupMulti =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
createGroup2 "club" alice cath
threadDelay 1000000
alice #> "#team hi"
bob <# "#team alice> hi"
msgId1 <- lastItemId alice
threadDelay 1000000
bob #> "#team hey"
alice <# "#team bob> hey"
msgId2 <- lastItemId alice
alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2)
alice <# "#club <- you #team"
alice <## " hi"
alice <# "#club <- #team"
alice <## " hey"
cath <# "#club alice> -> forwarded"
cath <## " hi"
cath <# "#club alice> -> forwarded"
cath <## " hey"
-- read chat
alice ##> "/tail #club 2"
alice <# "#club <- you #team"
alice <## " hi"
alice <# "#club <- #team"
alice <## " hey"
cath ##> "/tail #club 2"
cath <# "#club alice> -> forwarded"
cath <## " hi"
cath <# "#club alice> -> forwarded"
cath <## " hey"
testMultiForwardFiles :: HasCallStack => FilePath -> IO ()
testMultiForwardFiles =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp"
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp"
setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp"
connectUsers alice bob
connectUsers bob cath
threadDelay 1000000
msgIdZero <- lastItemId bob
bob #> "@alice hi"
alice <# "bob> hi"
-- send original files
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "@bob message without file"
alice <# "@bob sending file 1"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <# "@bob sending file 2"
alice <# "/f @bob test.pdf"
alice <## "use /fc 2 to cancel sending"
bob <# "alice> message without file"
bob <# "alice> sending file 1"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <# "alice> sending file 2"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob"
alice <## "completed uploading file 2 (test.pdf) for bob"
bob ##> "/fr 1"
bob
<### [ "saving file 1 from alice to test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob ##> "/fr 2"
bob
<### [ "saving file 2 from alice to test.pdf",
"started receiving file 2 (test.pdf) from alice"
]
bob <## "completed receiving file 2 (test.pdf) from alice"
src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg"
dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg"
dest1 `shouldBe` src1
src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf"
dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf"
dest2 `shouldBe` src2
-- forward file
let msgId1 = (read msgIdZero :: Int) + 1
bob ##> ("/_forward @3 @2 " <> show msgId1 <> "," <> show (msgId1 + 1) <> "," <> show (msgId1 + 2) <> "," <> show (msgId1 + 3))
-- messages printed for bob
bob <# "@cath <- you @alice"
bob <## " hi"
bob <# "@cath <- @alice"
bob <## " message without file"
bob <# "@cath <- @alice"
bob <## " sending file 1"
bob <# "/f @cath test_1.jpg"
bob <## "use /fc 3 to cancel sending"
bob <# "@cath <- @alice"
bob <## " sending file 2"
bob <# "/f @cath test_1.pdf"
bob <## "use /fc 4 to cancel sending"
-- messages printed for cath
cath <# "bob> -> forwarded"
cath <## " hi"
cath <# "bob> -> forwarded"
cath <## " message without file"
cath <# "bob> -> forwarded"
cath <## " sending file 1"
cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "bob> -> forwarded"
cath <## " sending file 2"
cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 2 [<dir>/ | <path>] to receive it"
-- file transfer
bob <## "completed uploading file 3 (test_1.jpg) for cath"
bob <## "completed uploading file 4 (test_1.pdf) for cath"
cath ##> "/fr 1"
cath
<### [ "saving file 1 from bob to test_1.jpg",
"started receiving file 1 (test_1.jpg) from bob"
]
cath <## "completed receiving file 1 (test_1.jpg) from bob"
cath ##> "/fr 2"
cath
<### [ "saving file 2 from bob to test_1.pdf",
"started receiving file 2 (test_1.pdf) from bob"
]
cath <## "completed receiving file 2 (test_1.pdf) from bob"
src1B <- B.readFile "./tests/tmp/bob_app_files/test_1.jpg"
src1B `shouldBe` dest1
dest1C <- B.readFile "./tests/tmp/cath_app_files/test_1.jpg"
dest1C `shouldBe` src1B
src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf"
src2B `shouldBe` dest2
dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf"
dest2C `shouldBe` src2B
-- deleting original file doesn't delete forwarded file
checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do
bob ##> "/clear alice"
bob <## "alice: all messages are removed locally ONLY"
fwdFileExists <- doesFileExist "./tests/tmp/bob_app_files/test_1.jpg"
fwdFileExists `shouldBe` True

View file

@ -14,6 +14,7 @@ import Control.Monad (forM_, void, when)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.List (intercalate, isInfixOf) import Data.List (intercalate, isInfixOf)
import qualified Data.Text as T import qualified Data.Text as T
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options import Simplex.Chat.Options
import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Protocol (supportedChatVRange)
@ -64,7 +65,10 @@ chatGroupTests = do
it "moderate message of another group member (full delete)" testGroupModerateFullDelete it "moderate message of another group member (full delete)" testGroupModerateFullDelete
it "moderate message that arrives after the event of moderation" testGroupDelayedModeration it "moderate message that arrives after the event of moderation" testGroupDelayedModeration
it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete
describe "batch send messages" $ do
it "send multiple messages api" testSendMulti it "send multiple messages api" testSendMulti
it "send multiple timed messages" testSendMultiTimed
it "send multiple messages (many chat batches)" testSendMultiManyBatches
describe "async group connections" $ do describe "async group connections" $ do
xit "create and join group when clients go offline" testGroupAsync xit "create and join group when clients go offline" testGroupAsync
describe "group links" $ do describe "group links" $ do
@ -1305,26 +1309,29 @@ testGroupMessageDeleteMultipleManyBatches =
cath ##> "/set receipts all off" cath ##> "/set receipts all off"
cath <## "ok" cath <## "ok"
alice #> "#team message 0" msgIdZero <- lastItemId alice
concurrently_
(bob <# "#team alice> message 0") let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
(cath <# "#team alice> message 0") cms = intercalate ", " (map cm [1 .. 300 :: Int])
msgIdFirst <- lastItemId alice
alice `send` ("/_send #1 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i -> do forM_ [(1 :: Int) .. 300] $ \i -> do
alice #> ("#team message " <> show i)
concurrently_ concurrently_
(bob <# ("#team alice> message " <> show i)) (bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i))
msgIdLast <- lastItemId alice msgIdLast <- lastItemId alice
let mIdFirst = read msgIdFirst :: Int let mIdFirst = (read msgIdZero :: Int) + 1
mIdLast = read msgIdLast :: Int mIdLast = read msgIdLast :: Int
deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) deleteIds = intercalate "," (map show [mIdFirst .. mIdLast])
alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast") alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast")
_ <- getTermLine alice _ <- getTermLine alice
alice <## "301 messages deleted" alice <## "300 messages deleted"
forM_ [(0 :: Int) .. 300] $ \i -> forM_ [(1 :: Int) .. 300] $ \i ->
concurrently_ concurrently_
(bob <# ("#team alice> [marked deleted] message " <> show i)) (bob <# ("#team alice> [marked deleted] message " <> show i))
(cath <# ("#team alice> [marked deleted] message " <> show i)) (cath <# ("#team alice> [marked deleted] message " <> show i))
@ -1821,15 +1828,89 @@ testGroupDelayedModerationFullDelete tmp = do
testSendMulti :: HasCallStack => FilePath -> IO () testSendMulti :: HasCallStack => FilePath -> IO ()
testSendMulti = testSendMulti =
testChat2 aliceProfile bobProfile $ testChat3 aliceProfile bobProfile cathProfile $
\alice bob -> do \alice bob cath -> do
createGroup2 "team" alice bob createGroup3 "team" alice bob cath
alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "#team test 1" alice <# "#team test 1"
alice <# "#team test 2" alice <# "#team test 2"
bob <# "#team alice> test 1" bob <# "#team alice> test 1"
bob <# "#team alice> test 2" bob <# "#team alice> test 2"
cath <# "#team alice> test 1"
cath <# "#team alice> test 2"
testSendMultiTimed :: HasCallStack => FilePath -> IO ()
testSendMultiTimed =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/set disappear #team on 1"
alice <## "updated group preferences:"
alice <## "Disappearing messages: on (1 sec)"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Disappearing messages: on (1 sec)"
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Disappearing messages: on (1 sec)"
alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "#team test 1"
alice <# "#team test 2"
bob <# "#team alice> test 1"
bob <# "#team alice> test 2"
cath <# "#team alice> test 1"
cath <# "#team alice> test 2"
alice
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
bob
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
cath
<### [ "timed message deleted: test 1",
"timed message deleted: test 2"
]
testSendMultiManyBatches :: HasCallStack => FilePath -> IO ()
testSendMultiManyBatches =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
msgIdAlice <- lastItemId alice
msgIdBob <- lastItemId bob
msgIdCath <- lastItemId cath
let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}"
cms = intercalate ", " (map cm [1 .. 300 :: Int])
alice `send` ("/_send #1 json [" <> cms <> "]")
_ <- getTermLine alice
alice <## "300 messages sent"
forM_ [(1 :: Int) .. 300] $ \i -> do
concurrently_
(bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i))
aliceItemsCount <- withCCTransaction alice $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]]
aliceItemsCount `shouldBe` [[300]]
bobItemsCount <- withCCTransaction bob $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]]
cathItemsCount <- withCCTransaction cath $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]]
cathItemsCount `shouldBe` [[300]]
testGroupAsync :: HasCallStack => FilePath -> IO () testGroupAsync :: HasCallStack => FilePath -> IO ()
testGroupAsync tmp = do testGroupAsync tmp = do

View file

@ -17,12 +17,14 @@ chatLocalChatsTests :: SpecWith FilePath
chatLocalChatsTests = do chatLocalChatsTests = do
describe "note folders" $ do describe "note folders" $ do
it "create folders, add notes, read, search" testNotes it "create folders, add notes, read, search" testNotes
it "create multiple messages api" testCreateMulti
it "switch users" testUserNotes it "switch users" testUserNotes
it "preview pagination for notes" testPreviewsPagination it "preview pagination for notes" testPreviewsPagination
it "chat pagination" testChatPagination it "chat pagination" testChatPagination
it "stores files" testFiles it "stores files" testFiles
it "deleting files does not interfere with other chat types" testOtherFiles it "deleting files does not interfere with other chat types" testOtherFiles
describe "batch create messages" $ do
it "create multiple messages api" testCreateMulti
it "create multiple messages with files" testCreateMultiFiles
testNotes :: FilePath -> IO () testNotes :: FilePath -> IO ()
testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
@ -53,14 +55,6 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
alice ##> "/tail *" alice ##> "/tail *"
alice <# "* Greetings." alice <# "* Greetings."
testCreateMulti :: FilePath -> IO ()
testCreateMulti tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "* test 1"
alice <# "* test 2"
testUserNotes :: FilePath -> IO () testUserNotes :: FilePath -> IO ()
testUserNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do testUserNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice createCCNoteFolder alice
@ -197,3 +191,36 @@ testOtherFiles =
doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True
where where
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
testCreateMulti :: FilePath -> IO ()
testCreateMulti tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "* test 1"
alice <# "* test 2"
testCreateMultiFiles :: FilePath -> IO ()
testCreateMultiFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
createCCNoteFolder alice
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg"
copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf"
let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}"
cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}"
cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}"
alice ##> ("/_create *1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]")
alice <# "* message without file"
alice <# "* sending file 1"
alice <# "* file 1 (test.jpg)"
alice <# "* sending file 2"
alice <# "* file 2 (test.pdf)"
doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` True
doesFileExist "./tests/tmp/alice_app_files/test.pdf" `shouldReturn` True
alice ##> "/_get chat *1 count=3"
r <- chatF <$> getTermLine alice
r `shouldBe` [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]