{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} module ChatTests.Direct where import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) import Control.Monad (forM_, when) import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.Text as T import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion, supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, VersionChat, pattern VersionChat) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Version import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.FilePath ((>)) import Test.Hspec hiding (it) chatDirectTests :: SpecWith FilePath chatDirectTests = do describe "direct messages" $ do describe "add contact and send/receive messages" testAddContact it "clear chat with contact" testContactClear it "deleting contact deletes profile" testDeleteContactDeletesProfile it "unused contact is deleted silently" testDeleteUnusedContactSilent it "direct message quoted replies" testDirectMessageQuotedReply it "direct message update" testDirectMessageUpdate it "direct message edit history" testDirectMessageEditHistory it "direct message delete" testDirectMessageDelete it "direct live message" testDirectLiveMessage it "direct timed message" testDirectTimedMessage it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "should send multiline message" testMultilineMessage describe "duplicate contacts" $ do it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate describe "invitation link connection plan" $ do it "invitation link ok to connect" testPlanInvitationLinkOk it "own invitation link" testPlanInvitationLinkOwn it "connecting via invitation link" testPlanInvitationLinkConnecting describe "SMP servers" $ do it "get and set SMP servers" testGetSetSMPServers it "test SMP server connection" testTestSMPServerConnection describe "XFTP servers" $ do it "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer describe "async connection handshake" $ do it "connect when initiating client goes offline" testAsyncInitiatingOffline it "connect when accepting client goes offline" testAsyncAcceptingOffline describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do -- fails in CI xit'' "v2" testFullAsync describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall describe "maintenance mode" $ do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles it "encrypt/decrypt database" testDatabaseEncryption describe "coordination between app and NSE" $ do it "should not subscribe in NSE and subscribe in the app" testSubscribeAppNSE describe "mute/unmute messages" $ do it "mute/unmute contact" testMuteContact it "mute/unmute group and member" testMuteGroup describe "multiple users" $ do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses it "create user with default servers" testCreateUserDefaultServers it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages it "user profile privacy: hide profiles and notificaitons" testUserPrivacy describe "settings" $ do it "set chat item expiration TTL" testSetChatItemTTL it "save/get app settings" testAppSettings describe "connection switch" $ do it "switch contact to a different queue" testSwitchContact it "stop switching contact to a different queue" testAbortSwitchContact it "switch group member to a different queue" testSwitchGroupMember it "stop switching group member to a different queue" testAbortSwitchGroupMember describe "connection verification code" $ do it "verificationCode function converts ByteString to series of digits" $ \_ -> verificationCode (C.sha256Hash "abcd") `shouldBe` "61889 38426 63934 09576 96390 79389 84124 85253 63658 69469 70853 37788 95900 68296 20156 25" it "sameVerificationCode function should ignore spaces" $ \_ -> sameVerificationCode "123 456 789" "12345 6789" `shouldBe` True it "mark contact verified" testMarkContactVerified it "mark group member verified" testMarkGroupMemberVerified describe "message errors" $ do it "show message decryption error" testMsgDecryptError it "should report ratchet de-synchronization, synchronize ratchets" testSyncRatchet it "synchronize ratchets, reset connection code" testSyncRatchetCodeReset describe "message reactions" $ do it "set message reactions" testSetMessageReactions describe "delivery receipts" $ do it "should send delivery receipts" testSendDeliveryReceipts it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts describe "negotiate connection peer chat protocol version range" $ do describe "peer version range correctly set for new connection via invitation" $ do testInvVRange (supportedChatVRange PQSupportOff) (supportedChatVRange PQSupportOff) testInvVRange (supportedChatVRange PQSupportOff) vr11 testInvVRange vr11 (supportedChatVRange PQSupportOff) testInvVRange vr11 vr11 describe "peer version range correctly set for new connection via contact request" $ do testReqVRange (supportedChatVRange PQSupportOff) (supportedChatVRange PQSupportOff) testReqVRange (supportedChatVRange PQSupportOff) vr11 testReqVRange vr11 (supportedChatVRange PQSupportOff) testReqVRange vr11 vr11 it "update peer version range on received messages" testUpdatePeerChatVRange describe "network statuses" $ do it "should get network statuses" testGetNetworkStatuses describe "PQ tests" $ do describe "enable PQ before connection, connect via invitation link" $ pqMatrix2 runTestPQConnectViaLink describe "enable PQ before connection, connect via contact address" $ pqMatrix2 runTestPQConnectViaAddress describe "connect via invitation link with PQ encryption enabled" $ pqVersionTestMatrix2 runTestPQVersionsViaLink describe "connect via contact address with PQ encryption enabled" $ pqVersionTestMatrix2 runTestPQVersionsViaAddress it "should enable PQ after several messages in connection without PQ" testPQEnableContact it "should enable PQ, reduce envelope size and enable compression" testPQEnableContactCompression where testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 testAddContact :: HasCallStack => SpecWith FilePath testAddContact = versionTestMatrix2 runTestAddContact where runTestAddContact alice bob = do alice ##> "/_connect 1" inv <- getInvitation alice bob ##> ("/_connect 1 " <> inv) bob <## "confirmation sent!" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 chatsEmpty alice bob alice #> "@bob hello there 🙂" bob <# "alice> hello there 🙂" alice ##> "/_unread chat @2 on" alice <## "ok" alice ##> "/_unread chat @2 off" alice <## "ok" chatsOneMessage alice bob bob #> "@alice hello there" alice <# "bob> hello there" bob #> "@alice how are you?" alice <# "bob> how are you?" chatsManyMessages alice bob chatsEmpty alice bob = do alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) bob @@@ [("@alice", lastChatFeature)] bob #$> ("/_get chat @2 count=100", chat, chatFeatures) chatsOneMessage alice bob = do alice @@@ [("@bob", "hello there 🙂")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) bob @@@ [("@alice", "hello there 🙂")] bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂")]) chatsManyMessages alice bob = do alice @@@ [("@bob", "how are you?")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) bob @@@ [("@alice", "how are you?")] bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) -- pagination alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) -- search alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages alice #$> ("/_read chat @2 from=1 to=100", id, "ok") bob #$> ("/_read chat @2 from=1 to=100", id, "ok") alice #$> ("/_read chat @2", id, "ok") bob #$> ("/_read chat @2", id, "ok") alice #$> ("/read user", id, "ok") alice #$> ("/_read user 1", id, "ok") testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsSeparate = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob alice ##> "/c" inv' <- getInvitation alice bob ##> ("/c " <> inv') bob <## "confirmation sent!" concurrently_ (alice <## "bob_1 (Bob): contact is connected") (bob <## "alice_1 (Alice): contact is connected") alice <##> bob alice #> "@bob_1 1" bob <# "alice_1> 1" bob #> "@alice_1 2" alice <# "bob_1> 2" alice @@@ [("@bob", "hey"), ("@bob_1", "2")] alice `hasContactProfiles` ["alice", "bob", "bob"] bob @@@ [("@alice", "hey"), ("@alice_1", "2")] bob `hasContactProfiles` ["bob", "alice", "alice"] testDuplicateContactsMultipleSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsMultipleSeparate = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob alice ##> "/c" inv' <- getInvitation alice bob ##> ("/c " <> inv') bob <## "confirmation sent!" concurrently_ (alice <## "bob_1 (Bob): contact is connected") (bob <## "alice_1 (Alice): contact is connected") alice ##> "/c" inv'' <- getInvitation alice bob ##> ("/c " <> inv'') bob <## "confirmation sent!" concurrently_ (alice <## "bob_2 (Bob): contact is connected") (bob <## "alice_2 (Alice): contact is connected") alice <##> bob alice #> "@bob_1 1" bob <# "alice_1> 1" bob #> "@alice_1 2" alice <# "bob_1> 2" alice #> "@bob_2 3" bob <# "alice_2> 3" bob #> "@alice_2 4" alice <# "bob_2> 4" alice ##> "/contacts" alice <### ["bob (Bob)", "bob_1 (Bob)", "bob_2 (Bob)"] bob ##> "/contacts" bob <### ["alice (Alice)", "alice_1 (Alice)", "alice_2 (Alice)"] alice `hasContactProfiles` ["alice", "bob", "bob", "bob"] bob `hasContactProfiles` ["bob", "alice", "alice", "alice"] testPlanInvitationLinkOk :: HasCallStack => FilePath -> IO () testPlanInvitationLinkOk = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/c" inv <- getInvitation alice bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" bob ##> ("/c " <> inv) bob <## "confirmation sent!" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection alice <##> bob testPlanInvitationLinkOwn :: HasCallStack => FilePath -> IO () testPlanInvitationLinkOwn tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/c" inv <- getInvitation alice alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: own link" let invSchema2 = linkAnotherSchema inv alice ##> ("/_connect plan 1 " <> invSchema2) alice <## "invitation link: own link" alice ##> ("/c " <> inv) alice <## "confirmation sent!" alice <### [ "alice_1 (Alice): contact is connected", "alice_2 (Alice): contact is connected" ] alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" alice <### [ WithTime "@alice_2 hi", WithTime "alice_1> hi" ] alice `send` "@alice_1 hey" alice <### [ WithTime "@alice_1 hey", WithTime "alice_2> hey" ] alice @@@ [("@alice_1", "hey"), ("@alice_2", "hey")] testPlanInvitationLinkConnecting :: HasCallStack => FilePath -> IO () testPlanInvitationLinkConnecting tmp = do inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/c" getInvitation alice withNewTestChat tmp "bob" bobProfile $ \bob -> do bob ##> ("/c " <> inv) bob <## "confirmation sent!" bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: connecting" let invSchema2 = linkAnotherSchema inv bob ##> ("/_connect plan 1 " <> invSchema2) bob <## "invitation link: connecting" testContactClear :: HasCallStack => FilePath -> IO () testContactClear = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob threadDelay 500000 alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY") alice #$> ("/_get chat @2 count=100", chat, []) bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") bob #$> ("/_get chat @2 count=100", chat, []) testDeleteContactDeletesProfile :: HasCallStack => FilePath -> IO () testDeleteContactDeletesProfile = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob -- alice deletes contact, profile is deleted alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" alice ##> "/_contacts 1" (alice ) alice `hasContactProfiles` ["alice"] -- bob deletes contact, profile is deleted bob ##> "/d alice" bob <## "alice: contact is deleted" bob ##> "/contacts" (bob ) bob `hasContactProfiles` ["bob"] testDeleteUnusedContactSilent :: HasCallStack => FilePath -> IO () testDeleteUnusedContactSilent = testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath bob ##> "/contacts" bob <### ["alice (Alice)", "cath (Catherine)"] bob `hasContactProfiles` ["bob", "alice", "cath"] cath ##> "/contacts" cath <### ["alice (Alice)", "bob (Bob)"] cath `hasContactProfiles` ["cath", "alice", "bob"] -- bob deletes cath, cath's bob contact is deleted silently bob ##> "/d cath" bob <## "cath: contact is deleted" bob ##> "/contacts" bob <## "alice (Alice)" threadDelay 50000 cath ##> "/contacts" cath <## "alice (Alice)" -- group messages work alice #> "#team hello" concurrentlyN_ [ bob <# "#team alice> hello", cath <# "#team alice> hello" ] bob #> "#team hi there" concurrentlyN_ [ alice <# "#team bob> hi there", cath <# "#team bob> hi there" ] cath #> "#team hey" concurrentlyN_ [ alice <# "#team cath> hey", bob <# "#team cath> hey" ] testDirectMessageQuotedReply :: HasCallStack => FilePath -> IO () testDirectMessageQuotedReply = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice ##> "/_send @2 text hello! how are you?" alice <# "@bob hello! how are you?" bob <# "alice> hello! how are you?" bob #> "@alice hi!" alice <# "bob> hi!" bob `send` "> @alice (hello) all good - you?" bob <# "@alice > hello! how are you?" bob <## " all good - you?" alice <# "bob> > hello! how are you?" alice <## " all good - you?" bob #$> ("/_get chat @2 count=1", chat', [((1, "all good - you?"), Just (0, "hello! how are you?"))]) alice #$> ("/_get chat @2 count=1", chat', [((0, "all good - you?"), Just (1, "hello! how are you?"))]) bob `send` ">> @alice (all good) will tell more" bob <# "@alice >> all good - you?" bob <## " will tell more" alice <# "bob> >> all good - you?" alice <## " will tell more" bob #$> ("/_get chat @2 count=1", chat', [((1, "will tell more"), Just (1, "all good - you?"))]) alice #$> ("/_get chat @2 count=1", chat', [((0, "will tell more"), Just (0, "all good - you?"))]) testDirectMessageUpdate :: HasCallStack => FilePath -> IO () testDirectMessageUpdate = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- msg id 1 alice #> "@bob hello 🙂" bob <# "alice> hello 🙂" -- msg id 2 bob `send` "> @alice (hello) hi alice" bob <# "@alice > hello 🙂" bob <## " hi alice" alice <# "bob> > hello 🙂" alice <## " hi alice" alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hello 🙂"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) alice ##> ("/_update item @2 " <> itemId 1 <> " text hello 🙂") alice <## "message didn't change" alice ##> ("/_update item @2 " <> itemId 1 <> " text hey 👋") alice <# "@bob [edited] hey 👋" bob <# "alice> [edited] hey 👋" alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) -- msg id 3 bob `send` "> @alice (hey) hey alice" bob <# "@alice > hey 👋" bob <## " hey alice" alice <# "bob> > hey 👋" alice <## " hey alice" alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) alice ##> ("/_update item @2 " <> itemId 1 <> " text greetings 🤝") alice <# "@bob [edited] greetings 🤝" bob <# "alice> [edited] greetings 🤝" alice #$> ("/_update item @2 " <> itemId 2 <> " text updating bob's message", id, "cannot update this item") alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) bob ##> ("/_update item @2 " <> itemId 2 <> " text hey Alice") bob <# "@alice [edited] > hello 🙂" bob <## " hey Alice" alice <# "bob> [edited] > hello 🙂" alice <## " hey Alice" bob ##> ("/_update item @2 " <> itemId 3 <> " text greetings Alice") bob <# "@alice [edited] > hey 👋" bob <## " greetings Alice" alice <# "bob> [edited] > hey 👋" alice <## " greetings Alice" alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "greetings 🤝"), Nothing), ((0, "hey Alice"), Just (1, "hello 🙂")), ((0, "greetings Alice"), Just (1, "hey 👋"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "greetings 🤝"), Nothing), ((1, "hey Alice"), Just (0, "hello 🙂")), ((1, "greetings Alice"), Just (0, "hey 👋"))]) testDirectMessageEditHistory :: HasCallStack => FilePath -> IO () testDirectMessageEditHistory = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice #> "@bob hello!" bob <# "alice> hello!" alice ##> ("/_get item info @2 " <> itemId 1) alice <##. "sent at: " alice <## "message history:" alice .<## ": hello!" bob ##> ("/_get item info @2 " <> itemId 1) bob <##. "sent at: " bob <##. "received at: " bob <## "message history:" bob .<## ": hello!" alice ##> ("/_update item @2 " <> itemId 1 <> " text hey 👋") alice <# "@bob [edited] hey 👋" bob <# "alice> [edited] hey 👋" alice ##> ("/_get item info @2 " <> itemId 1) alice <##. "sent at: " alice <## "message history:" alice .<## ": hey 👋" alice .<## ": hello!" bob ##> ("/_get item info @2 " <> itemId 1) bob <##. "sent at: " bob <##. "received at: " bob <## "message history:" bob .<## ": hey 👋" bob .<## ": hello!" alice ##> ("/_update item @2 " <> itemId 1 <> " text hello there") alice <# "@bob [edited] hello there" bob <# "alice> [edited] hello there" alice ##> "/item info @bob hello" alice <##. "sent at: " alice <## "message history:" alice .<## ": hello there" alice .<## ": hey 👋" alice .<## ": hello!" bob ##> "/item info @alice hello" bob <##. "sent at: " bob <##. "received at: " bob <## "message history:" bob .<## ": hello there" bob .<## ": hey 👋" bob .<## ": hello!" bob #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") alice ##> ("/_update item @2 " <> itemId 1 <> " text hey there") alice <# "@bob [edited] hey there" bob <# "alice> [edited] hey there" alice ##> "/item info @bob hey" alice <##. "sent at: " alice <## "message history:" alice .<## ": hey there" alice .<## ": hello there" alice .<## ": hey 👋" alice .<## ": hello!" bob ##> "/item info @alice hey" bob <##. "sent at: " bob <##. "received at: " bob <## "message history:" bob .<## ": hey there" testDirectMessageDelete :: HasCallStack => FilePath -> IO () testDirectMessageDelete = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- Test for exception not interrupting the delivery - uncomment lines in newContentMessage -- alice #> "@bob hello 111" -- bob <## "exception: user error (#####################)" -- -- bob <## "bad chat command: #####################" -- -- bob <# "alice> hello 111" -- alice, bob: msg id 1 alice #> "@bob hello 🙂" bob <# "alice> hello 🙂" -- alice, bob: msg id 2 bob `send` "> @alice (hello 🙂) hey alic" bob <# "@alice > hello 🙂" bob <## " hey alic" alice <# "bob> > hello 🙂" alice <## " hey alic" -- alice: deletes msg ids 1,2 alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) -- alice: msg id 1 bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice") bob <# "@alice [edited] > hello 🙂" bob <## " hey alice" alice <# "bob> [edited] hey alice" alice @@@ [("@bob", "hey alice")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice")]) -- bob: marks deleted msg id 2 bob #$> ("/_delete item @2 " <> itemId 2 <> " broadcast", id, "message marked deleted") bob @@@ [("@alice", "hey alice [marked deleted]")] alice <# "bob> [marked deleted] hey alice" alice @@@ [("@bob", "hey alice [marked deleted]")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")]) -- alice: deletes msg id 1 that was broadcast deleted by bob alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) -- alice: msg id 1, bob: msg id 3 (quoting message alice deleted locally) bob `send` "> @alice (hello 🙂) do you receive my messages?" bob <# "@alice > hello 🙂" bob <## " do you receive my messages?" alice <# "bob> > hello 🙂" alice <## " do you receive my messages?" alice @@@ [("@bob", "do you receive my messages?")] alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item") -- alice: msg id 2, bob: msg id 4 bob #> "@alice how are you?" alice <# "bob> how are you?" -- alice: deletes msg id 2 alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") -- bob: marks deleted msg id 4 (that alice deleted locally) bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted") alice <## "bob> [deleted - original message not found]" alice @@@ [("@bob", "do you receive my messages?")] alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) bob @@@ [("@alice", "how are you? [marked deleted]")] bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "hey alice [marked deleted]"), Just (0, "hello 🙂")), ((1, "do you receive my messages?"), Just (0, "hello 🙂")), ((1, "how are you? [marked deleted]"), Nothing)]) -- bob: deletes msg ids 2,4 (that he has marked deleted) bob #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted") bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) testDirectLiveMessage :: HasCallStack => FilePath -> IO () testDirectLiveMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- non-empty live message is sent instantly alice `send` "/live @bob hello" bob <# "alice> [LIVE started] use /show [on/off/7] hello" alice ##> ("/_update item @2 " <> itemId 1 <> " text hello there") alice <# "@bob [LIVE] hello there" bob <# "alice> [LIVE ended] hello there" -- empty live message is also sent instantly alice `send` "/live @bob" bob <# "alice> [LIVE started] use /show [on/off/8]" alice ##> ("/_update item @2 " <> itemId 2 <> " text hello 2") alice <# "@bob [LIVE] hello 2" bob <# "alice> [LIVE ended] hello 2" -- live message has edit history alice ##> ("/_get item info @2 " <> itemId 2) alice <##. "sent at: " alice <## "message history:" alice .<## ": hello 2" alice .<## ":" bob ##> ("/_get item info @2 " <> itemId 2) bob <##. "sent at: " bob <##. "received at: " bob <## "message history:" bob .<## ": hello 2" bob .<## ":" testDirectTimedMessage :: HasCallStack => FilePath -> IO () testDirectTimedMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice ##> "/_send @2 ttl=1 text hello!" alice <# "@bob hello!" bob <# "alice> hello!" alice <## "timed message deleted: hello!" bob <## "timed message deleted: hello!" alice ##> "/_send @2 live=off ttl=1 text hey" alice <# "@bob hey" bob <# "alice> hey" alice <## "timed message deleted: hey" bob <## "timed message deleted: hey" alice ##> "/_send @2 ttl=default text hello" alice <# "@bob hello" bob <# "alice> hello" alice ##> "/_send @2 live=off text hi" alice <# "@bob hi" bob <# "alice> hi" testRepeatAuthErrorsDisableContact :: HasCallStack => FilePath -> IO () testRepeatAuthErrorsDisableContact = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob threadDelay 500000 bob ##> "/_delete @2 notify=off" bob <## "alice: contact is deleted" forM_ [1 .. authErrDisableCount] $ \_ -> sendAuth alice alice <## "[bob] connection is disabled, to enable: /enable bob, to delete: /d bob" alice ##> "@bob hey" alice <## "bob: disabled, to enable: /enable bob, to delete: /d bob" alice ##> "/enable bob" alice <## "ok" sendAuth alice where sendAuth alice = do alice #> "@bob hey" alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" testMultilineMessage :: HasCallStack => FilePath -> IO () testMultilineMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do connectUsers alice bob connectUsers alice cath alice `send` "@bob \"hello\\nthere\"" -- @bob "hello\nthere" alice <# "@bob hello" alice <## "there" bob <# "alice> hello" bob <## "there" alice `send` "/feed \"hello\\nthere\"" -- /feed "hello\nthere" alice <##. "/feed (2)" alice <## "there" bob <# "alice> hello" bob <## "there" cath <# "alice> hello" cath <## "there" testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ \alice _ -> do alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im") alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") alice ##> "/smp" alice <## "smp://2345-w==@smp2.example.im" alice <## "smp://3456-w==@smp3.example.im:5224" alice #$> ("/smp default", id, "ok") alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") testTestSMPServerConnection :: HasCallStack => FilePath -> IO () testTestSMPServerConnection = testChat2 aliceProfile bobProfile $ \alice _ -> do alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001" alice <## "SMP server test passed" -- to test with password: -- alice <## "SMP server test failed at CreateQueue, error: SMP AUTH" -- alice <## "Server requires authorization to create queues, check password" alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK" alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" testGetSetXFTPServers :: HasCallStack => FilePath -> IO () testGetSetXFTPServers = testChat2 aliceProfile bobProfile $ \alice _ -> withXFTPServer $ do alice #$> ("/_servers 1 xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im") alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok") alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im") alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") alice ##> "/xftp" alice <## "xftp://2345-w==@xftp2.example.im" alice <## "xftp://3456-w==@xftp3.example.im:5224" alice #$> ("/xftp default", id, "ok") alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") testTestXFTPServer :: HasCallStack => FilePath -> IO () testTestXFTPServer = testChat2 aliceProfile bobProfile $ \alice _ -> withXFTPServer $ do alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002" alice <## "XFTP server test passed" -- to test with password: -- alice <## "XFTP server test failed at CreateFile, error: XFTP AUTH" -- alice <## "Server requires authorization to upload files, check password" alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice <## "XFTP server test passed" alice ##> "/xftp test xftp://LcJU@localhost:7002" alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK" alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO () testAsyncInitiatingOffline tmp = do putStrLn "testAsyncInitiatingOffline" inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" getInvitation alice withNewTestChat tmp "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" withTestChat tmp "alice" $ \alice -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") testAsyncAcceptingOffline :: HasCallStack => FilePath -> IO () testAsyncAcceptingOffline tmp = do putStrLn "testAsyncAcceptingOffline" inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/c" getInvitation alice withNewTestChat tmp "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" withTestChat tmp "alice" $ \alice -> do withTestChat tmp "bob" $ \bob -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") testFullAsync :: HasCallStack => FilePath -> IO () testFullAsync tmp = do putStrLn "testFullAsync" inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" getInvitation alice withNewTestChat tmp "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" withTestChat tmp "alice" $ \_ -> pure () -- connecting... notification in UI withTestChat tmp "bob" $ \_ -> pure () -- connecting... notification in UI withTestChat tmp "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "bob (Bob): contact is connected" withTestChat tmp "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "alice (Alice): contact is connected" testFullAsyncV1 :: HasCallStack => FilePath -> IO () testFullAsyncV1 tmp = do putStrLn "testFullAsyncV1" inv <- withNewAlice $ \alice -> do putStrLn "1" alice ##> "/c" putStrLn "2" getInvitation alice putStrLn "3" withNewBob $ \bob -> do putStrLn "4" bob ##> ("/c " <> inv) putStrLn "5" bob <## "confirmation sent!" putStrLn "6" withAlice $ \_ -> pure () putStrLn "7" withBob $ \_ -> pure () putStrLn "8" withAlice $ \alice -> do putStrLn "9" alice <## "1 contacts connected (use /cs for the list)" putStrLn "10" withBob $ \_ -> pure () putStrLn "11" withAlice $ \alice -> do putStrLn "12" alice <## "1 contacts connected (use /cs for the list)" putStrLn "13" alice <## "bob (Bob): contact is connected" putStrLn "14" withBob $ \bob -> do putStrLn "15" bob <## "1 contacts connected (use /cs for the list)" putStrLn "16" bob <## "alice (Alice): contact is connected" where withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile withAlice = withTestChatV1 tmp "alice" withNewBob = withNewTestChatV1 tmp "bob" bobProfile withBob = withTestChatV1 tmp "bob" testFullAsyncV1toV2 :: HasCallStack => FilePath -> IO () testFullAsyncV1toV2 tmp = do putStrLn "testFullAsyncV1toV2" inv <- withNewAlice $ \alice -> do putStrLn "1" alice ##> "/c" putStrLn "2" getInvitation alice putStrLn "3" withNewBob $ \bob -> do putStrLn "4" bob ##> ("/c " <> inv) putStrLn "5" bob <## "confirmation sent!" withAlice $ \_ -> pure () putStrLn "6" withBob $ \_ -> pure () putStrLn "7" withAlice $ \alice -> do putStrLn "8" alice <## "1 contacts connected (use /cs for the list)" putStrLn "9" withBob $ \_ -> pure () putStrLn "10" withAlice $ \alice -> do putStrLn "11" alice <## "1 contacts connected (use /cs for the list)" putStrLn "12" alice <## "bob (Bob): contact is connected" putStrLn "13" withBob $ \bob -> do putStrLn "14" bob <## "1 contacts connected (use /cs for the list)" putStrLn "15" bob <## "alice (Alice): contact is connected" where withNewAlice = withNewTestChat tmp "alice" aliceProfile withAlice = withTestChat tmp "alice" withNewBob = withNewTestChatV1 tmp "bob" bobProfile withBob = withTestChatV1 tmp "bob" testFullAsyncV2toV1 :: HasCallStack => FilePath -> IO () testFullAsyncV2toV1 tmp = do putStrLn "testFullAsyncV2toV1" inv <- withNewAlice $ \alice -> do putStrLn "1" alice ##> "/c" putStrLn "2" getInvitation alice putStrLn "3" withNewBob $ \bob -> do putStrLn "4" bob ##> ("/c " <> inv) putStrLn "5" bob <## "confirmation sent!" putStrLn "6" withAlice $ \_ -> pure () putStrLn "7" withBob $ \_ -> pure () putStrLn "8" withAlice $ \alice -> do putStrLn "9" alice <## "1 contacts connected (use /cs for the list)" putStrLn "10" withBob $ \_ -> pure () putStrLn "11" withAlice $ \alice -> do putStrLn "12" alice <## "1 contacts connected (use /cs for the list)" putStrLn "13" alice <## "bob (Bob): contact is connected" putStrLn "14" withBob $ \bob -> do putStrLn "15" bob <## "1 contacts connected (use /cs for the list)" putStrLn "16" bob <## "alice (Alice): contact is connected" where withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile {-# INLINE withNewAlice #-} withAlice = withTestChatV1 tmp "alice" {-# INLINE withAlice #-} withNewBob = withNewTestChat tmp "bob" bobProfile {-# INLINE withNewBob #-} withBob = withTestChat tmp "bob" {-# INLINE withBob #-} testCallType :: CallType testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}} testWebRTCSession :: WebRTCSession testWebRTCSession = WebRTCSession { rtcSession = "{}", rtcIceCandidates = "[]" } testWebRTCCallOffer :: WebRTCCallOffer testWebRTCCallOffer = WebRTCCallOffer { callType = testCallType, rtcSession = testWebRTCSession } serialize :: ToJSON a => a -> String serialize = B.unpack . LB.toStrict . J.encode repeatM_ :: Int -> IO a -> IO () repeatM_ n a = forM_ [1 .. n] $ const a testNegotiateCall :: HasCallStack => FilePath -> IO () testNegotiateCall = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- just for testing db query alice ##> "/_call get" -- alice invite bob to call alice ##> ("/_call invite @2 " <> serialize testCallType) alice <## "ok" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: calling...")]) bob <## "alice wants to connect with you via WebRTC video call (e2e encrypted)" repeatM_ 3 $ getTermLine bob bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: calling...")]) -- bob accepts call by sending WebRTC offer bob ##> ("/_call offer @2 " <> serialize testWebRTCCallOffer) bob <## "ok" bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: accepted")]) alice <## "bob accepted your WebRTC video call (e2e encrypted)" repeatM_ 3 $ getTermLine alice alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: accepted")]) -- alice confirms call by sending WebRTC answer alice ##> ("/_call answer @2 " <> serialize testWebRTCSession) alice <## "ok" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: connecting...")]) bob <## "alice continued the WebRTC call" repeatM_ 3 $ getTermLine bob bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: connecting...")]) -- participants can update calls as connected alice ##> "/_call status @2 connected" alice <## "ok" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: in progress (00:00)")]) bob ##> "/_call status @2 connected" bob <## "ok" bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: in progress (00:00)")]) -- either party can end the call bob ##> "/_call end @2" bob <## "ok" bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: ended (00:00)")]) alice <## "call with bob ended" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: ended (00:00)")]) testMaintenanceMode :: HasCallStack => FilePath -> IO () testMaintenanceMode tmp = do withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do alice ##> "/c" alice <## "error: chat not started" alice ##> "/_start" alice <## "chat started" connectUsers alice bob alice #> "@bob hi" bob <# "alice> hi" alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" alice <## "error: chat not stopped" alice ##> "/_stop" alice <## "chat stopped" alice ##> "/_start" alice <## "chat started" -- chat works after start alice <## "1 contacts connected (use /cs for the list)" alice #> "@bob hi again" bob <# "alice> hi again" bob #> "@alice hello" alice <# "bob> hello" -- export / delete / import alice ##> "/_stop" alice <## "chat stopped" alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" alice <## "ok" doesFileExist "./tests/tmp/alice-chat.zip" `shouldReturn` True alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" alice <## "ok" -- cannot start chat after import alice ##> "/_start" alice <## "error: chat store changed, please restart chat" -- works after full restart withTestChat tmp "alice" $ \alice -> testChatWorking alice bob testChatWorking :: HasCallStack => TestCC -> TestCC -> IO () testChatWorking alice bob = do alice <## "1 contacts connected (use /cs for the list)" alice #> "@bob hello again" bob <# "alice> hello again" bob #> "@alice hello too" alice <# "bob> hello too" testMaintenanceModeWithFiles :: HasCallStack => FilePath -> IO () testMaintenanceModeWithFiles tmp = withXFTPServer $ do withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do alice ##> "/_start" alice <## "chat started" alice ##> "/_files_folder ./tests/tmp/alice_files" alice <## "ok" connectUsers alice bob bob #> "/f @alice ./tests/fixtures/test.jpg" bob <## "use /fc 1 to cancel sending" alice <# "bob> sends file test.jpg (136.5 KiB / 139737 bytes)" alice <## "use /fr 1 [