simplex-chat/tests/ChatTests/Groups.hs
Evgeny 621b291da1
core: member mentions, types and rfc (#5555)
* core: member mentions, types and rfc

* update

* update rfc

* save/get mentions (WIP)

* markdown

* store received mentions and userMention flag

* sent mentions

* update message with mentions

* db queries

* CLI mentions, test passes

* use maps for mentions

* tests

* comment

* save mentions on sent messages

* postresql schema

* refactor

* M.empty

* include both displayName and localAlias into MentionedMemberInfo

* fix saving sent mentions

* include mentions in previews

* update plans
2025-01-29 13:04:48 +00:00

6705 lines
253 KiB
Haskell

{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module ChatTests.Groups where
import ChatClient
import ChatTests.DBUtils
import ChatTests.Utils
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import Control.Monad (forM_, void, when)
import Data.Bifunctor (second)
import qualified Data.ByteString.Char8 as B
import Data.List (intercalate, isInfixOf)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Library.Internal (uniqueMsgMentions)
import Simplex.Chat.Messages (ChatItemId)
import Simplex.Chat.Options
import Simplex.Chat.Protocol (MemberMention (..), supportedChatVRange)
import Simplex.Chat.Types (MemberId (..), VersionRangeChat)
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.RetryInterval
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Server.Env.STM hiding (subscriptions)
import Simplex.Messaging.Transport
import Test.Hspec hiding (it)
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..))
#else
import Database.SQLite.Simple (Only (..))
import Simplex.Chat.Options.DB
import System.Directory (copyFile)
import System.FilePath ((</>))
#endif
chatGroupTests :: SpecWith TestParams
chatGroupTests = do
describe "chat groups" $ do
describe "add contacts, create group and send/receive messages" testGroupMatrix
it "mark multiple messages as read" testMarkReadGroup
it "initial chat pagination" testChatPaginationInitial
it "v1: add contacts, create group and send/receive messages" testGroup
it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
it "send large message" testGroupLargeMessage
it "create group with incognito membership" testNewGroupIncognito
it "create and join group with 4 members" testGroup2
it "create and delete group" testGroupDelete
it "create group with the same displayName" testGroupSameName
it "invitee delete group when in status invited" testGroupDeleteWhenInvited
it "re-add member in status invited" testGroupReAddInvited
it "re-add member in status invited, change role" testGroupReAddInvitedChangeRole
it "delete contact before they accept group invitation, contact joins group" testGroupDeleteInvitedContact
it "member profile is kept when deleting group if other groups have this member" testDeleteGroupMemberProfileKept
it "remove contact from group and add again" testGroupRemoveAdd
it "list groups containing group invitations" testGroupList
it "group message quoted replies" testGroupMessageQuotedReply
it "group message update" testGroupMessageUpdate
it "group message edit history" testGroupMessageEditHistory
it "group message delete" testGroupMessageDelete
it "group message delete multiple" testGroupMessageDeleteMultiple
it "group message delete multiple (many chat batches)" testGroupMessageDeleteMultipleManyBatches
it "group live message" testGroupLiveMessage
it "update group profile" testUpdateGroupProfile
it "update member role" testUpdateMemberRole
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
it "group description is shown as the first message to new members" testGroupDescription
it "moderate message of another group member" testGroupModerate
it "moderate own message (should process as deletion)" testGroupModerateOwn
it "moderate multiple messages" testGroupModerateMultiple
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 (full delete)" testGroupDelayedModerationFullDelete
describe "batch send messages" $ do
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
xit "create and join group when clients go offline" testGroupAsync
describe "group links" $ do
it "create group link, join via group link" testGroupLink
it "delete group, re-join via same link" testGroupLinkDeleteGroupRejoin
it "sending message to contact created via group link marks it used" testGroupLinkContactUsed
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
it "unused host contact is deleted after all groups with it are deleted" testGroupLinkUnusedHostContactDeleted
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
it "group link member role" testGroupLinkMemberRole
it "leaving and deleting the group joined via link should NOT delete previously existing direct contacts" testGroupLinkLeaveDelete
describe "group link connection plan" $ do
it "group link ok to connect; known group" testPlanGroupLinkOkKnown
it "group is known if host contact was deleted" testPlanHostContactDeletedGroupLinkKnown
it "own group link" testPlanGroupLinkOwn
it "connecting via group link" testPlanGroupLinkConnecting
it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin
describe "group links without contact" $ do
it "join via group link without creating contact" testGroupLinkNoContact
it "invitees were previously connected as contacts" testGroupLinkNoContactInviteesWereConnected
it "all members were previously connected as contacts" testGroupLinkNoContactAllMembersWereConnected
it "group link member role" testGroupLinkNoContactMemberRole
it "host incognito" testGroupLinkNoContactHostIncognito
it "invitee incognito" testGroupLinkNoContactInviteeIncognito
it "host profile received" testGroupLinkNoContactHostProfileReceived
it "existing contact merged" testGroupLinkNoContactExistingContactMerged
describe "group links without contact connection plan" $ do
it "group link without contact - known group" testPlanGroupLinkNoContactKnown
it "group link without contact - connecting" testPlanGroupLinkNoContactConnecting
it "group link without contact - connecting (slow handshake)" testPlanGroupLinkNoContactConnectingSlow
#if !defined(dbPostgres)
-- TODO [postgres] restore from outdated db backup (same as in agent)
describe "group message errors" $ do
it "show message decryption error" testGroupMsgDecryptError
it "should report ratchet de-synchronization, synchronize ratchets" testGroupSyncRatchet
it "synchronize ratchets, reset connection code" testGroupSyncRatchetCodeReset
#endif
describe "group message reactions" $ do
it "set group message reactions" testSetGroupMessageReactions
describe "group delivery receipts" $ do
it "should send delivery receipts in group" testSendGroupDeliveryReceipts
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
describe "direct connections in group are not established based on chat protocol version" $ do
describe "3 members group" $ do
testNoDirect _0 _0 True
testNoDirect _0 _1 True
testNoDirect _1 _0 False
testNoDirect _1 _1 False
it "members have different local display names in different groups" testNoDirectDifferentLDNs
describe "merge members and contacts" $ do
it "new member should merge with existing contact" testMergeMemberExistingContact
it "new contact should merge with existing member" testMergeContactExistingMember
it "new contact should merge with multiple existing members" testMergeContactMultipleMembers
it "new group link host contact should merge with single existing contact out of multiple" testMergeGroupLinkHostMultipleContacts
describe "create member contact" $ do
it "create contact with group member with invitation message" testMemberContactMessage
it "create contact with group member without invitation message" testMemberContactNoMessage
it "prohibited to create contact with group member if it already exists" testMemberContactProhibitedContactExists
it "prohibited to repeat sending x.grp.direct.inv" testMemberContactProhibitedRepeatInv
it "invited member replaces member contact reference if it already exists" testMemberContactInvitedConnectionReplaced
it "share incognito profile" testMemberContactIncognito
it "sends and updates profile when creating contact" testMemberContactProfileUpdate
it "re-create member contact after deletion, many groups" testRecreateMemberContactManyGroups
describe "group message forwarding" $ do
it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForward
it "deduplicate forwarded messages" testGroupMsgForwardDeduplicate
it "forward message edit (x.msg.update)" testGroupMsgForwardEdit
it "forward message reaction (x.msg.react)" testGroupMsgForwardReaction
it "forward message deletion (x.msg.del)" testGroupMsgForwardDeletion
it "forward file (x.msg.file.descr)" testGroupMsgForwardFile
it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole
it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember
it "forward member leaving (x.grp.leave)" testGroupMsgForwardLeave
describe "group history" $ do
it "text messages" testGroupHistory
it "history is sent when joining via group link" testGroupHistoryGroupLink
it "history is not sent if preference is disabled" testGroupHistoryPreferenceOff
it "host's file" testGroupHistoryHostFile
it "member's file" testGroupHistoryMemberFile
it "large file with text" testGroupHistoryLargeFile
it "multiple files" testGroupHistoryMultipleFiles
it "cancelled files are not attached (text message is still sent)" testGroupHistoryFileCancel
it "cancelled files without text are excluded" testGroupHistoryFileCancelNoText
it "quoted messages" testGroupHistoryQuotes
it "deleted message is not included" testGroupHistoryDeletedMessage
it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage
it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage
it "unknown member messages are processed" testGroupHistoryUnknownMember
describe "membership profile updates" $ do
it "send profile update on next message to group" testMembershipProfileUpdateNextGroupMessage
it "multiple groups with same member, update is applied only once" testMembershipProfileUpdateSameMember
it "member contact is active" testMembershipProfileUpdateContactActive
it "member contact is deleted" testMembershipProfileUpdateContactDeleted
it "member contact is deleted silently, then considered disabled" testMembershipProfileUpdateContactDisabled
it "profile update without change is ignored" testMembershipProfileUpdateNoChangeIgnored
it "change of profile contact link is ignored" testMembershipProfileUpdateContactLinkIgnored
describe "block member for all" $ do
it "messages are marked blocked" testBlockForAllMarkedBlocked
it "messages are fully deleted" testBlockForAllFullDelete
it "another admin can unblock" testBlockForAllAnotherAdminUnblocks
it "member was blocked before joining group" testBlockForAllBeforeJoining
it "can't repeat block, unblock" testBlockForAllCantRepeat
describe "group member inactivity" $ do
it "mark member inactive on reaching quota" testGroupMemberInactive
describe "group member reports" $ do
it "should send report to group owner, admins and moderators, but not other users" testGroupMemberReports
describe "group member mentions" $ do
it "should send messages with member mentions" testMemberMention
describe "uniqueMsgMentions" testUniqueMsgMentions
where
_0 = supportedChatVRange -- don't create direct connections
_1 = groupCreateDirectVRange
-- having host configured with older version doesn't have effect in tests
-- because host uses current code and sends version in MemberInfo
testNoDirect vrMem2 vrMem3 noConns =
it
( "host "
<> vRangeStr supportedChatVRange
<> (", 2nd mem " <> vRangeStr vrMem2)
<> (", 3rd mem " <> vRangeStr vrMem3)
<> (if noConns then " : 2 <!!> 3" else " : 2 <##> 3")
)
$ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns
testGroup :: HasCallStack => TestParams -> IO ()
testGroup =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> testGroupShared alice bob cath False True
testGroupCheckMessages :: HasCallStack => TestParams -> IO ()
testGroupCheckMessages =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> testGroupShared alice bob cath True True
testGroupMatrix :: SpecWith TestParams
testGroupMatrix =
versionTestMatrix3 $ \alice bob cath -> testGroupShared alice bob cath False False
testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> Bool -> IO ()
testGroupShared alice bob cath checkMessages directConnections = do
connectUsers alice bob
connectUsers alice cath
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob admin"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "use /j team to accept"
]
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
when checkMessages $ threadDelay 1000000 -- for deterministic order of messages and "connected" events
alice ##> "/a team cath admin"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to cath",
do
cath <## "#team: alice invites you to join the group as admin"
cath <## "use /j team to accept"
]
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <## "#team: member bob (Bob) is connected",
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
when checkMessages $ threadDelay 1000000 -- for deterministic order of messages and "connected" events
alice #> "#team hello"
msgItem1 <- lastItemId alice
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
when checkMessages $ threadDelay 1000000 -- server assigns timestamps with one second precision
bob #> "#team hi there"
concurrently_
(alice <# "#team bob> hi there")
(cath <# "#team bob> hi there")
when checkMessages $ threadDelay 1000000
cath #> "#team hey team"
concurrently_
(alice <# "#team cath> hey team")
(bob <# "#team cath> hey team")
msgItem2 <- lastItemId alice
when directConnections $
bob <##> cath
when checkMessages $ getReadChats msgItem1 msgItem2
-- list groups
alice ##> "/gs"
alice <## "#team (3 members)"
-- list group members
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): admin, invited, connected",
"cath (Catherine): admin, invited, connected"
]
-- list contacts
alice ##> "/contacts"
alice <## "bob (Bob)"
alice <## "cath (Catherine)"
-- test observer role
alice ##> "/mr team bob observer"
concurrentlyN_
[ alice <## "#team: you changed the role of bob from admin to observer",
bob <## "#team: alice changed your role from admin to observer",
cath <## "#team: alice changed the role of bob from admin to observer"
]
bob ##> "#team hello"
bob <## "#team: you don't have permission to send messages"
bob ##> "/rm team cath"
bob <## "#team: you have insufficient permissions for this action, the required role is admin"
cath #> "#team hello"
concurrentlyN_
[ alice <# "#team cath> hello",
bob <# "#team cath> hello"
]
alice ##> "/mr team bob admin"
concurrentlyN_
[ alice <## "#team: you changed the role of bob from observer to admin",
bob <## "#team: alice changed your role from observer to admin",
cath <## "#team: alice changed the role of bob from observer to admin"
]
-- remove member
bob ##> "/rm team cath"
concurrentlyN_
[ bob <## "#team: you removed cath from the group",
alice <## "#team: bob removed cath from the group",
do
cath <## "#team: bob removed you from the group"
cath <## "use /d #team to delete the group"
]
bob #> "#team hi"
concurrently_
(alice <# "#team bob> hi")
(cath </)
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath </)
cath ##> "#team hello"
cath <## "you are no longer a member of the group"
when directConnections $
bob <##> cath
-- delete contact
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
when checkMessages $ threadDelay 1000000
alice #> "#team checking connection"
bob <# "#team alice> checking connection"
when checkMessages $ threadDelay 1000000
bob #> "#team received"
alice <# "#team bob> received"
when checkMessages $ do
alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")]
bob @@@ [("@alice", "contact deleted"), ("@cath", "hey"), ("#team", "received")]
-- test clearing chat
threadDelay 1000000
alice #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
alice #$> ("/_get chat #1 count=100", chat, [])
bob #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
bob #$> ("/_get chat #1 count=100", chat, [])
cath #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
cath #$> ("/_get chat #1 count=100", chat, [])
where
getReadChats :: HasCallStack => String -> String -> IO ()
getReadChats msgItem1 msgItem2 = do
alice @@@ [("#team", "hey team"), ("@cath", "sent invitation to join group team as admin"), ("@bob", "sent invitation to join group team as admin")]
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")])
-- "before" and "after" define a chat item id across all chats,
-- so we take into account group event items as well as sent group invitations in direct chats
alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")])
alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")])
alice #$> ("/_get chat #1 around=" <> msgItem1 <> " count=2", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")])
alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")])
bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
cath #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "connected"), (0, "hello"), (0, "hi there"), (1, "hey team")])
alice #$> ("/_read chat #1", id, "ok")
bob #$> ("/_read chat #1", id, "ok")
cath #$> ("/_read chat #1", id, "ok")
alice #$> ("/_unread chat #1 on", id, "ok")
alice #$> ("/_unread chat #1 off", id, "ok")
testMarkReadGroup :: HasCallStack => TestParams -> IO ()
testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do
createGroup2 "team" alice bob
alice #> "#team 1"
alice #> "#team 2"
alice #> "#team 3"
alice #> "#team 4"
bob <# "#team alice> 1"
bob <# "#team alice> 2"
bob <# "#team alice> 3"
bob <# "#team alice> 4"
bob ##> "/last_item_id"
i :: ChatItemId <- read <$> getTermLine bob
let itemIds = intercalate "," $ map show [i - 3 .. i]
bob #$> ("/_read chat items #1 " <> itemIds, id, "ok")
testChatPaginationInitial :: HasCallStack => TestParams -> IO ()
testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do
createGroup2 "team" alice bob
-- Wait, otherwise ids are going to be wrong.
threadDelay 1000000
lastEventId <- (read :: String -> Int) <$> lastItemId bob
let groupItemId n = show $ lastEventId + n
-- Send messages from alice to bob
forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("#team " <> show n)
-- Bob receives the messages.
forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n)
-- All messages are unread for bob, should return area around unread
bob #$> ("/_get chat #1 initial=2", chat, [(0, "Recent history: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")])
-- Read next 2 items
let itemIds = intercalate "," $ map groupItemId [1 .. 2]
bob #$> ("/_read chat items #1 " <> itemIds, id, "ok")
bob #$> ("/_get chat #1 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")])
-- Read all items
bob #$> ("/_read chat #1", id, "ok")
bob #$> ("/_get chat #1 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")])
bob #$> ("/_get chat #1 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")])
where
opts =
testOpts
{ markRead = False
}
testGroupLargeMessage :: HasCallStack => TestParams -> IO ()
testGroupLargeMessage =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
img <- genProfileImg
let profileImage = "data:image/png;base64," <> B.unpack img
alice `send` ("/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\", \"groupPreferences\": {\"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}")
_trimmedCmd1 <- getTermLine alice
alice <## "profile image updated"
bob <## "alice updated group #team:"
bob <## "profile image updated"
testNewGroupIncognito :: HasCallStack => TestParams -> IO ()
testNewGroupIncognito =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
-- alice creates group with incognito membership
alice ##> "/g i team"
aliceIncognito <- getTermLine alice
alice <## ("group #team is created, your incognito profile for this group is " <> aliceIncognito)
alice <## "to add members use /create link #team"
-- alice invites bob
alice ##> "/a team bob"
alice <## "you are using an incognito profile for this group - prohibited to invite contacts"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
_ <- getTermLine alice
concurrentlyN_
[ do
alice <## ("bob_1 (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob_1 to print out this incognito profile again"
alice <## "bob_1 invited to group #team via your group link"
alice <## "#team: bob_1 joined the group",
do
bob <## (aliceIncognito <> ": contact is connected")
bob <## "#team: you joined the group"
]
alice <##> bob
alice ?#> "@bob_1 hi, I'm incognito"
bob <# (aliceIncognito <> "> hi, I'm incognito")
bob #> ("@" <> aliceIncognito <> " hey, I'm bob")
alice ?<# "bob_1> hey, I'm bob"
alice ?#> "#team hello"
bob <# ("#team " <> aliceIncognito <> "> hello")
bob #> "#team hi there"
alice ?<# "#team bob_1> hi there"
alice ##> "/gs"
alice <## "i #team (2 members)"
bob ##> "/gs"
bob <## "#team (2 members)"
testGroup2 :: HasCallStack => TestParams -> IO ()
testGroup2 =
testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
connectUsers alice bob
connectUsers alice cath
connectUsers bob dan
connectUsers alice dan
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob admin"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "use /j club to accept"
]
alice ##> "/a club cath admin"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as admin"
cath <## "use /j club to accept"
]
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
cath ##> "/j club"
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: you joined the group"
cath <## "#club: member bob (Bob) is connected",
do
bob <## "#club: alice added cath (Catherine) to the group (connecting...)"
bob <## "#club: new member cath is connected"
]
bob ##> "/a club dan"
concurrentlyN_
[ bob <## "invitation to join the group #club sent to dan",
do
dan <## "#club: bob invites you to join the group as member"
dan <## "use /j club to accept"
]
dan ##> "/j club"
concurrentlyN_
[ bob <## "#club: dan joined the group",
do
dan <## "#club: you joined the group"
dan
<### [ "#club: member alice_1 (Alice) is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#club: member cath (Catherine) is connected"
],
do
alice <## "#club: bob added dan_1 (Daniel) to the group (connecting...)"
alice <## "#club: new member dan_1 is connected"
alice <## "contact dan_1 is merged into dan"
alice <## "use @dan <message> to send messages",
do
cath <## "#club: bob added dan (Daniel) to the group (connecting...)"
cath <## "#club: new member dan is connected"
]
alice #> "#club hello"
concurrentlyN_
[ bob <# "#club alice> hello",
cath <# "#club alice> hello",
dan <# "#club alice> hello"
]
bob #> "#club hi there"
concurrentlyN_
[ alice <# "#club bob> hi there",
cath <# "#club bob> hi there",
dan <# "#club bob> hi there"
]
cath #> "#club hey"
concurrentlyN_
[ alice <# "#club cath> hey",
bob <# "#club cath> hey",
dan <# "#club cath> hey"
]
dan #> "#club how is it going?"
concurrentlyN_
[ alice <# "#club dan> how is it going?",
bob <# "#club dan> how is it going?",
cath <# "#club dan> how is it going?"
]
bob <##> cath
dan <##> cath
dan <##> alice
-- show last messages
alice ##> "/t #club 17"
alice -- these strings are expected in any order because of sorting by time and rounding of time for sent
<##?
( map (ConsoleString . ("#club " <> )) groupFeatureStrs
<>
[ "#club bob> connected",
"#club cath> connected",
"#club bob> added dan (Daniel)",
"#club dan> connected",
"#club hello",
"#club bob> hi there",
"#club cath> hey",
"#club dan> how is it going?"
]
)
alice ##> "/t @dan 2"
alice
<##? [ "dan> hi",
"@dan hey"
]
-- TODO this fails returning only 23 lines out of 24
-- alice ##> "/t 24"
-- alice
-- <##? [ "@bob sent invitation to join group club as admin",
-- "@cath sent invitation to join group club as admin",
-- "#club bob> connected",
-- "#club cath> connected",
-- "#club bob> added dan (Daniel)", -- either this is missing
-- "#club dan> connected",
-- "#club hello",
-- "#club bob> hi there",
-- "#club cath> hey",
-- "#club dan> how is it going?",
-- "dan> hi",
-- "@dan hey",
-- "dan> Disappearing messages: off",
-- "dan> Full deletion: off",
-- "dan> Voice messages: enabled",
-- "dan> Audio/video calls: enabled",
-- "bob> Disappearing messages: off", -- or this one
-- "bob> Full deletion: off",
-- "bob> Voice messages: enabled",
-- "bob> Audio/video calls: enabled",
-- "cath> Disappearing messages: off",
-- "cath> Full deletion: off",
-- "cath> Voice messages: enabled",
-- "cath> Audio/video calls: enabled"
-- ]
-- remove member
cath ##> "/rm club dan"
concurrentlyN_
[ cath <## "#club: you removed dan from the group",
alice <## "#club: cath removed dan from the group",
bob <## "#club: cath removed dan from the group",
do
dan <## "#club: cath removed you from the group"
dan <## "use /d #club to delete the group"
]
alice #> "#club hello"
concurrentlyN_
[ bob <# "#club alice> hello",
cath <# "#club alice> hello",
(dan </)
]
bob #> "#club hi there"
concurrentlyN_
[ alice <# "#club bob> hi there",
cath <# "#club bob> hi there",
(dan </)
]
cath #> "#club hey"
concurrentlyN_
[ alice <# "#club cath> hey",
bob <# "#club cath> hey",
(dan </)
]
dan ##> "#club how is it going?"
dan <## "you are no longer a member of the group"
dan ##> "/d #club"
dan <## "#club: you deleted the group"
dan <##> cath
dan <##> alice
-- member leaves
bob ##> "/l club"
concurrentlyN_
[ do
bob <## "#club: you left the group"
bob <## "use /d #club to delete the group",
alice <## "#club: bob left the group",
cath <## "#club: bob left the group"
]
alice #> "#club hello"
concurrently_
(cath <# "#club alice> hello")
(bob </)
cath #> "#club hey"
concurrently_
(alice <# "#club cath> hey")
(bob </)
bob ##> "#club how is it going?"
bob <## "you are no longer a member of the group"
bob ##> "/d #club"
bob <## "#club: you deleted the group"
bob <##> cath
bob <##> alice
testGroupDelete :: HasCallStack => TestParams -> IO ()
testGroupDelete =
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/d #team"
concurrentlyN_
[ alice <## "#team: you deleted the group",
do
bob <## "#team: alice deleted the group"
bob <## "use /d #team to delete the local copy of the group",
do
cath <## "#team: alice deleted the group"
cath <## "use /d #team to delete the local copy of the group"
]
alice ##> "#team hi"
alice <## "no group #team"
bob ##> "/d #team"
bob <## "#team: you deleted the group"
cath ##> "#team hi"
cath <## "you are no longer a member of the group"
cath ##> "/d #team"
cath <## "#team: you deleted the group"
alice <##> bob
alice <##> cath
-- unused group contacts are deleted
threadDelay 3000000
bob ##> "@cath hi"
bob <## "no contact cath"
(cath </)
cath ##> "@bob hi"
cath <## "no contact bob"
(bob </)
where
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
testGroupSameName :: HasCallStack => TestParams -> IO ()
testGroupSameName =
testChat2 aliceProfile bobProfile $
\alice _ -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/g team"
alice <## "group #team_1 is created"
alice <## "to add members use /a team_1 <name> or /create link #team_1"
testGroupDeleteWhenInvited :: HasCallStack => TestParams -> IO ()
testGroupDeleteWhenInvited =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
bob ##> "/d #team"
bob <## "#team: you deleted the group"
-- alice doesn't receive notification that bob deleted group,
-- but she can re-add bob
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
testGroupReAddInvited :: HasCallStack => TestParams -> IO ()
testGroupReAddInvited =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- alice re-adds bob, he sees it as the same group
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- if alice removes bob and then re-adds him, she uses a new connection request
-- and he sees it as a new group with a different local display name
alice ##> "/rm team bob"
alice <## "#team: you removed bob from the group"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team_1: alice invites you to join the group as member"
bob <## "use /j team_1 to accept"
]
testGroupReAddInvitedChangeRole :: HasCallStack => TestParams -> IO ()
testGroupReAddInvitedChangeRole =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
-- alice re-adds bob, he sees it as the same group
alice ##> "/a team bob owner"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as owner"
bob <## "use /j team to accept"
]
-- bob joins as owner
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
bob ##> "/d #team"
concurrentlyN_
[ bob <## "#team: you deleted the group",
do
alice <## "#team: bob deleted the group"
alice <## "use /d #team to delete the local copy of the group"
]
bob ##> "#team hi"
bob <## "no group #team"
alice ##> "/d #team"
alice <## "#team: you deleted the group"
testGroupDeleteInvitedContact :: HasCallStack => TestParams -> IO ()
testGroupDeleteInvitedContact =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
threadDelay 500000
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
alice `send` "@bob hey"
alice
<### [ WithTime "@bob hey",
"member #team bob does not have direct connection, creating",
"contact for member #team bob is created",
"sent invitation to connect directly to member #team bob"
]
bob
<### [ "#team alice is creating direct contact alice with you",
WithTime "alice> hey",
"alice: security code changed"
]
bob <## "alice (Alice): you can send messages to contact"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
alice <##> bob
testDeleteGroupMemberProfileKept :: HasCallStack => TestParams -> IO ()
testDeleteGroupMemberProfileKept =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
-- group 1
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
-- group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
alice #> "#club hello"
bob <# "#club alice> hello"
bob #> "#club hi there"
alice <# "#club bob> hi there"
-- delete contact
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "@bob hey"
alice <## "no contact bob, use @#club bob <your message>"
bob ##> "@alice hey"
bob <## "alice: not ready"
(alice </)
-- delete group 1
alice ##> "/d #team"
concurrentlyN_
[ alice <## "#team: you deleted the group",
do
bob <## "#team: alice deleted the group"
bob <## "use /d #team to delete the local copy of the group"
]
alice ##> "#team hi"
alice <## "no group #team"
bob ##> "/d #team"
bob <## "#team: you deleted the group"
-- group 2 still works
alice #> "#club checking connection"
bob <# "#club alice> checking connection"
bob #> "#club received"
alice <# "#club bob> received"
testGroupRemoveAdd :: HasCallStack => TestParams -> IO ()
testGroupRemoveAdd =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 100000
-- remove member
alice ##> "/rm team bob"
concurrentlyN_
[ alice <## "#team: you removed bob from the group",
do
bob <## "#team: alice removed you from the group"
bob <## "use /d #team to delete the group",
cath <## "#team: alice removed bob from the group"
]
threadDelay 100000
alice ##> "/a team bob"
alice <## "invitation to join the group #team sent to bob"
bob <## "#team_1: alice invites you to join the group as member"
bob <## "use /j team_1 to accept"
bob ##> "/j team_1"
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team_1: you joined the group"
bob <## "#team_1: member cath_1 (Catherine) is connected"
bob <## "contact cath_1 is merged into cath"
bob <## "use @cath <message> to send messages",
do
cath <## "#team: alice added bob_1 (Bob) to the group (connecting...)"
cath <## "#team: new member bob_1 is connected"
cath <## "contact bob_1 is merged into bob"
cath <## "use @bob <message> to send messages"
]
alice #> "#team hi"
concurrently_
(bob <# "#team_1 alice> hi")
(cath <# "#team alice> hi")
bob #> "#team_1 hey"
concurrently_
(alice <# "#team bob> hey")
(cath <# "#team bob> hey")
cath #> "#team hello"
concurrently_
(alice <# "#team cath> hello")
(bob <# "#team_1 cath> hello")
testGroupList :: HasCallStack => TestParams -> IO ()
testGroupList =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/g tennis"
alice <## "group #tennis is created"
alice <## "to add members use /a tennis <name> or /create link #tennis"
alice ##> "/a tennis bob"
concurrentlyN_
[ alice <## "invitation to join the group #tennis sent to bob",
do
bob <## "#tennis: alice invites you to join the group as member"
bob <## "use /j tennis to accept"
]
-- alice sees both groups
alice ##> "/gs"
alice <### ["#team (2 members)", "#tennis (1 member)"]
-- bob sees #tennis as invitation
bob ##> "/gs"
bob
<### [ "#team (2 members)",
"#tennis - you are invited (/j tennis to join, /d #tennis to delete invitation)"
]
-- after deleting invitation bob sees only one group
bob ##> "/d #tennis"
bob <## "#tennis: you deleted the group"
bob ##> "/gs"
bob <## "#team (2 members)"
testGroupMessageQuotedReply :: HasCallStack => TestParams -> IO ()
testGroupMessageQuotedReply =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hello! how are you?"
concurrently_
(bob <# "#team alice> hello! how are you?")
(cath <# "#team alice> hello! how are you?")
threadDelay 1000000
bob `send` "> #team @alice (hello) hello, all good, you?"
bob <# "#team > alice hello! how are you?"
bob <## " hello, all good, you?"
concurrently_
( do
alice <# "#team bob!> > alice hello! how are you?"
alice <## " hello, all good, you?"
)
( do
cath <# "#team bob> > alice hello! how are you?"
cath <## " hello, all good, you?"
)
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello! how are you?"), Nothing), ((1, "hello, all good, you?"), Just (0, "hello! how are you?"))])
alice #$> ("/_get chat #1 count=2", chat', [((1, "hello! how are you?"), Nothing), ((0, "hello, all good, you?"), Just (1, "hello! how are you?"))])
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello! how are you?"), Nothing), ((0, "hello, all good, you?"), Just (0, "hello! how are you?"))])
bob `send` "> #team bob (hello, all good) will tell more"
bob <# "#team > bob hello, all good, you?"
bob <## " will tell more"
concurrently_
( do
alice <# "#team bob> > bob hello, all good, you?"
alice <## " will tell more"
)
( do
cath <# "#team bob> > bob hello, all good, you?"
cath <## " will tell more"
)
bob #$> ("/_get chat #1 count=1", chat', [((1, "will tell more"), Just (1, "hello, all good, you?"))])
alice #$> ("/_get chat #1 count=1", chat', [((0, "will tell more"), Just (0, "hello, all good, you?"))])
cath #$> ("/_get chat #1 count=1", chat', [((0, "will tell more"), Just (0, "hello, all good, you?"))])
threadDelay 1000000
cath `send` "> #team bob (hello) hi there!"
cath <# "#team > bob hello, all good, you?"
cath <## " hi there!"
concurrently_
( do
alice <# "#team cath> > bob hello, all good, you?"
alice <## " hi there!"
)
( do
bob <# "#team cath!> > bob hello, all good, you?"
bob <## " hi there!"
)
cath #$> ("/_get chat #1 count=1", chat', [((1, "hi there!"), Just (0, "hello, all good, you?"))])
alice #$> ("/_get chat #1 count=1", chat', [((0, "hi there!"), Just (0, "hello, all good, you?"))])
bob #$> ("/_get chat #1 count=1", chat', [((0, "hi there!"), Just (1, "hello, all good, you?"))])
alice `send` "> #team (will tell) go on"
alice <# "#team > bob will tell more"
alice <## " go on"
concurrently_
( do
bob <# "#team alice!> > bob will tell more"
bob <## " go on"
)
( do
cath <# "#team alice> > bob will tell more"
cath <## " go on"
)
testGroupMessageUpdate :: HasCallStack => TestParams -> IO ()
testGroupMessageUpdate =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
-- alice, bob: msg id 5, cath: msg id 4 (after group invitations & group events)
alice #> "#team hello!"
concurrently_
(bob <# "#team alice> hello!")
(cath <# "#team alice> hello!")
msgItemId1 <- lastItemId alice
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hello!")
alice <## "message didn't change"
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hey 👋")
alice <# "#team [edited] hey 👋"
concurrently_
(bob <# "#team alice> [edited] hey 👋")
(cath <# "#team alice> [edited] hey 👋")
alice #$> ("/_get chat #1 count=1", chat', [((1, "hey 👋"), Nothing)])
bob #$> ("/_get chat #1 count=1", chat', [((0, "hey 👋"), Nothing)])
cath #$> ("/_get chat #1 count=1", chat', [((0, "hey 👋"), Nothing)])
threadDelay 1000000
-- alice, bob: msg id 6, cath: msg id 5
bob `send` "> #team @alice (hey) hi alice"
bob <# "#team > alice hey 👋"
bob <## " hi alice"
concurrently_
( do
alice <# "#team bob!> > alice hey 👋"
alice <## " hi alice"
)
( do
cath <# "#team bob> > alice hey 👋"
cath <## " hi alice"
)
alice #$> ("/_get chat #1 count=2", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hey 👋"))])
bob #$> ("/_get chat #1 count=2", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hey 👋"))])
cath #$> ("/_get chat #1 count=2", chat', [((0, "hey 👋"), Nothing), ((0, "hi alice"), Just (0, "hey 👋"))])
alice ##> ("/_update item #1 " <> msgItemId1 <> " text greetings 🤝")
alice <# "#team [edited] greetings 🤝"
concurrently_
(bob <# "#team alice> [edited] greetings 🤝")
(cath <# "#team alice> [edited] greetings 🤝")
msgItemId2 <- lastItemId alice
alice #$> ("/_update item #1 " <> msgItemId2 <> " text updating bob's message", id, "cannot update this item")
threadDelay 1000000
cath `send` "> #team @alice (greetings) greetings!"
cath <# "#team > alice greetings 🤝"
cath <## " greetings!"
concurrently_
( do
alice <# "#team cath!> > alice greetings 🤝"
alice <## " greetings!"
)
( do
bob <# "#team cath> > alice greetings 🤝"
bob <## " greetings!"
)
alice #$> ("/_get chat #1 count=3", chat', [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hey 👋")), ((0, "greetings!"), Just (1, "greetings 🤝"))])
bob #$> ("/_get chat #1 count=3", chat', [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hey 👋")), ((0, "greetings!"), Just (0, "greetings 🤝"))])
cath #$> ("/_get chat #1 count=3", chat', [((0, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (0, "hey 👋")), ((1, "greetings!"), Just (0, "greetings 🤝"))])
testGroupMessageEditHistory :: HasCallStack => TestParams -> IO ()
testGroupMessageEditHistory =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team hello!"
bob <# "#team alice> hello!"
aliceItemId <- lastItemId alice
bobItemId <- lastItemId bob
alice ##> ("/_get item info #1 " <> aliceItemId)
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hello!"
bob ##> ("/_get item info #1 " <> bobItemId)
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hello!"
alice ##> ("/_update item #1 " <> aliceItemId <> " text hey 👋")
alice <# "#team [edited] hey 👋"
bob <# "#team alice> [edited] hey 👋"
alice ##> ("/_get item info #1 " <> aliceItemId)
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hey 👋"
alice .<## ": hello!"
bob ##> ("/_get item info #1 " <> bobItemId)
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hey 👋"
bob .<## ": hello!"
alice ##> ("/_update item #1 " <> aliceItemId <> " text hello there")
alice <# "#team [edited] hello there"
bob <# "#team alice> [edited] hello there"
alice ##> "/item info #team hello"
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hello there"
alice .<## ": hey 👋"
alice .<## ": hello!"
bob ##> "/item info #team hello"
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hello there"
bob .<## ": hey 👋"
bob .<## ": hello!"
bob #$> ("/_delete item #1 " <> bobItemId <> " internal", id, "message deleted")
alice ##> ("/_update item #1 " <> aliceItemId <> " text hey there")
alice <# "#team [edited] hey there"
bob <# "#team alice> [edited] hey there"
alice ##> "/item info #team hey"
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hey there"
alice .<## ": hello there"
alice .<## ": hey 👋"
alice .<## ": hello!"
bob ##> "/item info #team hey"
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hey there"
testGroupMessageDelete :: HasCallStack => TestParams -> IO ()
testGroupMessageDelete =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
threadDelay 1000000
-- alice, bob: msg id 5, cath: msg id 4 (after group invitations & group events)
alice #> "#team hello!"
concurrently_
(bob <# "#team alice> hello!")
(cath <# "#team alice> hello!")
threadDelay 1000000
msgItemId1 <- lastItemId alice
alice #$> ("/_delete item #1 " <> msgItemId1 <> " internal", id, "message deleted")
alice #$> ("/_get chat #1 count=2", chat, [(0, "connected"), (1, "Full deletion: off")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "hello!")])
cath #$> ("/_get chat #1 count=1", chat, [(0, "hello!")])
threadDelay 1000000
-- alice: msg id 5, bob: msg id 6, cath: msg id 5
bob `send` "> #team @alice (hello) hi alic"
bob <# "#team > alice hello!"
bob <## " hi alic"
concurrently_
( do
alice <# "#team bob!> > alice hello!"
alice <## " hi alic"
)
( do
cath <# "#team bob> > alice hello!"
cath <## " hi alic"
)
alice #$> ("/_get chat #1 count=1", chat', [((0, "hi alic"), Just (1, "hello!"))])
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))])
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))])
msgItemId2 <- lastItemId alice
alice #$> ("/_delete item #1 " <> msgItemId2 <> " internal", id, "message deleted")
alice #$> ("/_get chat #1 count=2", chat', [((0, "connected"), Nothing), ((1, "Full deletion: off"), Nothing)])
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))])
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))])
-- alice: msg id 5
msgItemId3 <- lastItemId bob
bob ##> ("/_update item #1 " <> msgItemId3 <> " text hi alice")
bob <# "#team [edited] > alice hello!"
bob <## " hi alice"
concurrently_
(alice <# "#team bob> [edited] hi alice")
( do
cath <# "#team bob> [edited] > alice hello!"
cath <## " hi alice"
)
alice #$> ("/_get chat #1 count=1", chat', [((0, "hi alice"), Nothing)])
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!"))])
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!"))])
threadDelay 1000000
-- alice: msg id 6, bob: msg id 7, cath: msg id 6
cath #> "#team how are you?"
concurrently_
(alice <# "#team cath> how are you?")
(bob <# "#team cath> how are you?")
msgItemId4 <- lastItemId cath
cath #$> ("/_delete item #1 " <> msgItemId4 <> " broadcast", id, "message marked deleted")
concurrently_
(alice <# "#team cath> [marked deleted] how are you?")
(bob <# "#team cath> [marked deleted] how are you?")
alice ##> "/last_item_id 1"
msgItemId6 <- getTermLine alice
alice #$> ("/_delete item #1 " <> msgItemId6 <> " broadcast", id, "cannot delete this item")
alice #$> ("/_delete item #1 " <> msgItemId6 <> " internal", id, "message deleted")
alice #$> ("/_get chat #1 count=1", chat', [((0, "how are you? [marked deleted]"), Nothing)])
bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)])
cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)])
testGroupMessageDeleteMultiple :: HasCallStack => TestParams -> IO ()
testGroupMessageDeleteMultiple =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
msgId1 <- lastItemId alice
threadDelay 1000000
alice #> "#team hey"
concurrently_
(bob <# "#team alice> hey")
(cath <# "#team alice> hey")
msgId2 <- lastItemId alice
threadDelay 1000000
alice ##> ("/_delete item #1 " <> msgId1 <> "," <> msgId2 <> " broadcast")
alice <## "2 messages deleted"
concurrentlyN_
[ do
bob <# "#team alice> [marked deleted] hello"
bob <# "#team alice> [marked deleted] hey",
do
cath <# "#team alice> [marked deleted] hello"
cath <# "#team alice> [marked deleted] hey"
]
alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")])
bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")])
cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")])
testGroupMessageDeleteMultipleManyBatches :: HasCallStack => TestParams -> IO ()
testGroupMessageDeleteMultipleManyBatches =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
bob ##> "/set receipts all off"
bob <## "ok"
cath ##> "/set receipts all off"
cath <## "ok"
msgIdZero <- lastItemId alice
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))
msgIdLast <- lastItemId alice
let mIdFirst = (read msgIdZero :: Int) + 1
mIdLast = read msgIdLast :: Int
deleteIds = intercalate "," (map show [mIdFirst .. mIdLast])
alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast")
_ <- getTermLine alice
alice <## "300 messages deleted"
forM_ [(1 :: Int) .. 300] $ \i ->
concurrently_
(bob <# ("#team alice> [marked deleted] message " <> show i))
(cath <# ("#team alice> [marked deleted] message " <> show i))
testGroupLiveMessage :: HasCallStack => TestParams -> IO ()
testGroupLiveMessage =
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 500000
-- non-empty live message is sent instantly
alice `send` "/live #team hello"
msgItemId1 <- lastItemId alice
bob <#. "#team alice> [LIVE started]"
cath <#. "#team alice> [LIVE started]"
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hello there")
alice <# "#team [LIVE] hello there"
bob <# "#team alice> [LIVE ended] hello there"
cath <# "#team alice> [LIVE ended] hello there"
-- empty live message is also sent instantly
threadDelay 1000000
alice `send` "/live #team"
msgItemId2 <- lastItemId alice
bob <#. "#team alice> [LIVE started]"
cath <#. "#team alice> [LIVE started]"
alice ##> ("/_update item #1 " <> msgItemId2 <> " text hello 2")
alice <# "#team [LIVE] hello 2"
bob <# "#team alice> [LIVE ended] hello 2"
cath <# "#team alice> [LIVE ended] hello 2"
-- live message has edit history
alice ##> ("/_get item info #1 " <> msgItemId2)
alice <##. "sent at: "
alice <## "message history:"
alice .<## ": hello 2"
alice .<## ":"
bobItemId <- lastItemId bob
bob ##> ("/_get item info #1 " <> bobItemId)
bob <##. "sent at: "
bob <##. "received at: "
bob <## "message history:"
bob .<## ": hello 2"
bob .<## ":"
testUpdateGroupProfile :: HasCallStack => TestParams -> IO ()
testUpdateGroupProfile =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hello!"
concurrently_
(bob <# "#team alice> hello!")
(cath <# "#team alice> hello!")
bob ##> "/gp team my_team"
bob <## "#team: you have insufficient permissions for this action, the required role is owner"
alice ##> "/gp team my_team"
alice <## "changed to #my_team"
concurrentlyN_
[ do
bob <## "alice updated group #team:"
bob <## "changed to #my_team",
do
cath <## "alice updated group #team:"
cath <## "changed to #my_team"
]
bob #> "#my_team hi"
concurrently_
(alice <# "#my_team bob> hi")
(cath <# "#my_team bob> hi")
testUpdateMemberRole :: HasCallStack => TestParams -> IO ()
testUpdateMemberRole =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
addMember "team" alice bob GRAdmin
alice ##> "/mr team bob member"
alice <## "#team: you changed the role of bob from admin to member"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
connectUsers bob cath
bob ##> "/a team cath"
bob <## "#team: you have insufficient permissions for this action, the required role is admin"
alice ##> "/mr team bob admin"
concurrently_
(alice <## "#team: you changed the role of bob from member to admin")
(bob <## "#team: alice changed your role from member to admin")
bob ##> "/a team cath owner"
bob <## "#team: you have insufficient permissions for this action, the required role is owner"
addMember "team" bob cath GRMember
cath ##> "/j team"
concurrentlyN_
[ bob <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <## "#team: member alice (Alice) is connected",
do
alice <## "#team: bob added cath (Catherine) to the group (connecting...)"
alice <## "#team: new member cath is connected"
]
alice ##> "/mr team alice admin"
concurrentlyN_
[ alice <## "#team: you changed your role from owner to admin",
bob <## "#team: alice changed the role from owner to admin",
cath <## "#team: alice changed the role from owner to admin"
]
alice ##> "/d #team"
alice <## "#team: you have insufficient permissions for this action, the required role is owner"
testGroupDeleteUnusedContacts :: HasCallStack => TestParams -> IO ()
testGroupDeleteUnusedContacts =
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> do
-- create group 1
createGroup3 "team" alice bob cath
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
alice ##> "/a club cath"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as member"
cath <## "use /j club to accept"
]
cath ##> "/j club"
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: you joined the group"
cath <## "#club: member bob_1 (Bob) is connected"
cath <## "contact bob_1 is merged into bob"
cath <## "use @bob <message> to send messages",
do
bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)"
bob <## "#club: new member cath_1 is connected"
bob <## "contact cath_1 is merged into cath"
bob <## "use @cath <message> to send messages"
]
-- list contacts
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
cath ##> "/contacts"
cath <## "alice (Alice)"
cath <## "bob (Bob)"
-- delete group 1, contacts and profiles are kept
deleteGroup alice bob cath "team"
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath ##> "/contacts"
cath <## "alice (Alice)"
cath <## "bob (Bob)"
cath `hasContactProfiles` ["alice", "bob", "cath"]
-- delete group 2, unused contacts and profiles are deleted
deleteGroup alice bob cath "club"
threadDelay 3000000
bob ##> "/contacts"
bob <## "alice (Alice)"
bob `hasContactProfiles` ["alice", "bob"]
cath ##> "/contacts"
cath <## "alice (Alice)"
cath `hasContactProfiles` ["alice", "cath"]
where
cfg = mkCfgCreateGroupDirect $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
deleteGroup alice bob cath group = do
alice ##> ("/d #" <> group)
concurrentlyN_
[ alice <## ("#" <> group <> ": you deleted the group"),
do
bob <## ("#" <> group <> ": alice deleted the group")
bob <## ("use /d #" <> group <> " to delete the local copy of the group"),
do
cath <## ("#" <> group <> ": alice deleted the group")
cath <## ("use /d #" <> group <> " to delete the local copy of the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
cath ##> ("/d #" <> group)
cath <## ("#" <> group <> ": you deleted the group")
testGroupDescription :: HasCallStack => TestParams -> IO ()
testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/set delete #team off"
alice <## "updated group preferences:"
alice <## "Full deletion: off"
addMember "team" alice bob GRAdmin
bob ##> "/j team"
concurrentlyN_
[ alice <## "#team: bob joined the group",
bob <## "#team: you joined the group"
]
alice ##> "/group_profile team"
alice <## "#team"
groupInfo alice
alice ##> "/group_descr team Welcome to the team!"
alice <## "description changed to:"
alice <## "Welcome to the team!"
bob <## "alice updated group #team:"
bob <## "description changed to:"
bob <## "Welcome to the team!"
alice ##> "/group_profile team"
alice <## "#team"
alice <## "description:"
alice <## "Welcome to the team!"
groupInfo alice
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <# "#team alice> Welcome to the team!"
cath <## "#team: member bob (Bob) is connected",
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
connectUsers bob dan
addMember "team" bob dan GRMember
dan ##> "/j team"
concurrentlyN_
[ bob <## "#team: dan joined the group",
do
dan <## "#team: you joined the group"
dan <# "#team bob> Welcome to the team!"
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member cath (Catherine) is connected"
],
bobAddedDan alice,
bobAddedDan cath
]
where
groupInfo :: HasCallStack => TestCC -> IO ()
groupInfo alice = do
alice <## "group preferences:"
alice <## "Disappearing messages: off"
alice <## "Direct messages: on"
alice <## "Full deletion: off"
alice <## "Message reactions: on"
alice <## "Voice messages: on"
alice <## "Files and media: on"
alice <## "SimpleX links: on"
alice <## "Recent history: on"
bobAddedDan :: HasCallStack => TestCC -> IO ()
bobAddedDan cc = do
cc <## "#team: bob added dan (Daniel) to the group (connecting...)"
cc <## "#team: new member dan is connected"
testGroupModerate :: HasCallStack => TestParams -> IO ()
testGroupModerate =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
alice ##> "/mr team cath member"
concurrentlyN_
[ alice <## "#team: you changed the role of cath from admin to member",
bob <## "#team: alice changed the role of cath from admin to member",
cath <## "#team: alice changed your role from admin to member"
]
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
bob ##> "\\\\ #team @alice hello"
bob <## "cannot delete this item"
threadDelay 1000000
cath #> "#team hi"
concurrently_
(alice <# "#team cath> hi")
(bob <# "#team cath> hi")
bob ##> "\\\\ #team @cath hi"
bob <## "message marked deleted by you"
concurrently_
(alice <# "#team cath> [marked deleted by bob] hi")
(cath <# "#team cath> [marked deleted by bob] hi")
alice #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by bob]")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by you]")])
cath #$> ("/_get chat #1 count=1", chat, [(1, "hi [marked deleted by bob]")])
testGroupModerateOwn :: HasCallStack => TestParams -> IO ()
testGroupModerateOwn =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
disableFullDeletion2 "team" alice bob
threadDelay 1000000
alice #> "#team hello"
bob <# "#team alice> hello"
alice ##> "\\\\ #team @alice hello"
alice <## "message marked deleted by you"
bob <# "#team alice> [marked deleted by alice] hello"
alice #$> ("/_get chat #1 count=1", chat, [(1, "hello [marked deleted by you]")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "hello [marked deleted by alice]")])
testGroupModerateMultiple :: HasCallStack => TestParams -> IO ()
testGroupModerateMultiple =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
msgId1 <- lastItemId alice
threadDelay 1000000
bob #> "#team hey"
concurrently_
(alice <# "#team bob> hey")
(cath <# "#team bob> hey")
msgId2 <- lastItemId alice
alice ##> ("/_delete member item #1 " <> msgId1 <> "," <> msgId2)
alice <## "2 messages deleted"
concurrentlyN_
[ do
bob <# "#team alice> [marked deleted by alice] hello"
bob <# "#team bob> [marked deleted by alice] hey",
do
cath <# "#team alice> [marked deleted by alice] hello"
cath <# "#team bob> [marked deleted by alice] hey"
]
alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by you]"), (0, "hey [marked deleted by you]")])
bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (1, "hey [marked deleted by alice]")])
cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "hey [marked deleted by alice]")])
testGroupModerateFullDelete :: HasCallStack => TestParams -> IO ()
testGroupModerateFullDelete =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
alice ##> "/mr team cath member"
concurrentlyN_
[ alice <## "#team: you changed the role of cath from admin to member",
bob <## "#team: alice changed the role of cath from admin to member",
cath <## "#team: alice changed your role from admin to member"
]
alice ##> "/set delete #team on"
alice <## "updated group preferences:"
alice <## "Full deletion: on"
concurrentlyN_
[ do
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Full deletion: on",
do
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Full deletion: on"
]
threadDelay 1000000
cath #> "#team hi"
concurrently_
(alice <# "#team cath> hi")
(bob <# "#team cath> hi")
bob ##> "\\\\ #team @cath hi"
bob <## "message deleted by you"
concurrently_
(alice <# "#team cath> [deleted by bob] hi")
(cath <# "#team cath> [deleted by bob] hi")
alice #$> ("/_get chat #1 count=1", chat, [(0, "moderated [deleted by bob]")])
bob #$> ("/_get chat #1 count=1", chat, [(0, "moderated [deleted by you]")])
cath #$> ("/_get chat #1 count=1", chat, [(1, "moderated [deleted by bob]")])
testGroupDelayedModeration :: HasCallStack => TestParams -> IO ()
testGroupDelayedModeration ps = do
withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
disableFullDeletion2 "team" alice bob
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath <## "#team: you joined the group"
]
threadDelay 1000000
-- imitate not implemented group forwarding
-- (real client wouldn't have forwarding code, but tests use "current code" with configured version,
-- and forwarding client doesn't check compatibility)
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
cath #> "#team hi" -- message is pending for bob
alice <# "#team cath> hi"
alice ##> "\\\\ #team @cath hi"
alice <## "message marked deleted by you"
cath <# "#team cath> [marked deleted by alice] hi"
withTestChatCfg ps cfg "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
withTestChatCfg ps cfg "cath" $ \cath -> do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected"
bob
<### [ "#team: new member cath is connected",
EndsWith "#team cath> [marked deleted by alice] hi"
]
alice #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by you]")])
cath #$> ("/_get chat #1 count=2", chat, [(1, "hi [marked deleted by alice]"), (0, "connected")])
bob ##> "/_get chat #1 count=2"
r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")]
where
cfg = testCfgCreateGroupDirect
testGroupDelayedModerationFullDelete :: HasCallStack => TestParams -> IO ()
testGroupDelayedModerationFullDelete ps = do
withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
disableFullDeletion2 "team" alice bob
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath <## "#team: you joined the group"
]
threadDelay 1000000
-- imitate not implemented group forwarding
-- (real client wouldn't have forwarding code, but tests use "current code" with configured version,
-- and forwarding client doesn't check compatibility)
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
cath #> "#team hi" -- message is pending for bob
alice <# "#team cath> hi"
alice ##> "\\\\ #team @cath hi"
alice <## "message marked deleted by you"
cath <# "#team cath> [marked deleted by alice] hi"
-- if full deletion was enabled at time of moderation, cath would delete pending message as well,
-- that's why we set it afterwards to test delayed moderation for bob
alice ##> "/set delete #team on"
alice <## "updated group preferences:"
alice <## "Full deletion: on"
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Full deletion: on"
withTestChatCfg ps cfg "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Full deletion: on"
withTestChatCfg ps cfg "cath" $ \cath -> do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected"
bob
<### [ "#team: new member cath is connected",
EndsWith "#team cath> moderated [deleted by alice]"
]
alice #$> ("/_get chat #1 count=2", chat, [(0, "hi [marked deleted by you]"), (1, "Full deletion: on")])
cath #$> ("/_get chat #1 count=3", chat, [(1, "hi [marked deleted by alice]"), (0, "Full deletion: on"), (0, "connected")])
bob ##> "/_get chat #1 count=3"
r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")]
where
cfg = testCfgCreateGroupDirect
testSendMulti :: HasCallStack => TestParams -> IO ()
testSendMulti =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
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"
testSendMultiTimed :: HasCallStack => TestParams -> 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 => TestParams -> 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 => TestParams -> IO ()
testGroupAsync ps = do
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "use /j team to accept"
]
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
alice #> "#team hello bob"
bob <# "#team alice> hello bob"
withTestChat ps "alice" $ \alice -> do
withNewTestChat ps "cath" cathProfile $ \cath -> do
alice <## "1 contacts connected (use /cs for the list)"
alice <## "#team: connected to server(s)"
connectUsers alice cath
alice ##> "/a team cath"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to cath",
do
cath <## "#team: alice invites you to join the group as admin"
cath <## "use /j team to accept"
]
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath <## "#team: you joined the group"
]
alice #> "#team hello cath"
cath <# "#team alice> hello cath"
withTestChat ps "bob" $ \bob -> do
withTestChat ps "cath" $ \cath -> do
concurrentlyN_
[ do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <# "#team alice> hello cath"
bob <## "#team: new member cath is connected",
do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected"
]
threadDelay 500000
withTestChat ps "bob" $ \bob -> do
withNewTestChat ps "dan" danProfile $ \dan -> do
bob <## "2 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
connectUsers bob dan
bob ##> "/a team dan"
concurrentlyN_
[ bob <## "invitation to join the group #team sent to dan",
do
dan <## "#team: bob invites you to join the group as admin"
dan <## "use /j team to accept"
]
dan ##> "/j team"
concurrentlyN_
[ bob <## "#team: dan joined the group",
dan <## "#team: you joined the group"
]
threadDelay 1000000
threadDelay 1000000
withTestChat ps "alice" $ \alice -> do
withTestChat ps "cath" $ \cath -> do
withTestChat ps "dan" $ \dan -> do
concurrentlyN_
[ do
alice <## "2 contacts connected (use /cs for the list)"
alice <## "#team: connected to server(s)"
alice <## "#team: bob added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected",
do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: bob added dan (Daniel) to the group (connecting...)"
cath <## "#team: new member dan is connected",
do
dan <## "3 contacts connected (use /cs for the list)"
dan <## "#team: connected to server(s)"
dan <## "#team: member alice (Alice) is connected"
dan <## "#team: member cath (Catherine) is connected"
]
threadDelay 1000000
withTestChat ps "alice" $ \alice -> do
withTestChat ps "bob" $ \bob -> do
withTestChat ps "cath" $ \cath -> do
withTestChat ps "dan" $ \dan -> do
concurrentlyN_
[ do
alice <## "3 contacts connected (use /cs for the list)"
alice <## "#team: connected to server(s)",
do
bob <## "3 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)",
do
cath <## "3 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)",
do
dan <## "3 contacts connected (use /cs for the list)"
dan <## "#team: connected to server(s)"
]
alice #> "#team hello"
concurrentlyN_
[ bob <# "#team alice> hello",
cath <# "#team alice> hello",
dan <# "#team alice> hello"
]
bob #> "#team hi there"
concurrentlyN_
[ alice <# "#team bob> hi there",
cath <# "#team bob> hi there",
dan <# "#team bob> hi there"
]
cath #> "#team hey"
concurrentlyN_
[ alice <# "#team cath> hey",
bob <# "#team cath> hey",
dan <# "#team cath> hey"
]
dan #> "#team how is it going?"
concurrentlyN_
[ alice <# "#team dan> how is it going?",
bob <# "#team dan> how is it going?",
cath <# "#team dan> how is it going?"
]
bob <##> cath
dan <##> cath
dan <##> alice
testGroupLink :: HasCallStack => TestParams -> IO ()
testGroupLink =
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
\alice bob cath -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
alice ##> "/create link #team"
_ <- getGroupLink alice "team" GRMember True
alice ##> "/delete link #team"
alice <## "Group link is deleted - joined members will remain connected."
alice <## "To create a new group link use /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
alice ##> "/show link #team"
_ <- getGroupLink alice "team" GRMember False
alice ##> "/create link #team"
alice <## "you already have link for this group, to show: /show link #team"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")])
-- contacts connected via group link are not in chat previews
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice <##> bob
alice @@@ [("@bob", "hey"), ("#team", "connected")]
-- user address doesn't interfere
alice ##> "/ad"
cLink <- getContactLink alice True
cath ##> ("/c " <> cLink)
alice <#? cath
alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
concurrently_
(cath <## "alice (Alice): contact is connected")
(alice <## "cath (Catherine): contact is connected")
alice <##> cath
-- third member
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
alice <## "cath_1 (Catherine): accepting request to join group #team..."
-- if contact existed it is merged
concurrentlyN_
[ alice
<### [ "cath_1 (Catherine): contact is connected",
"contact cath_1 is merged into cath",
"use @cath <message> to send messages",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
cath
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#team: you joined the group",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
bob #> "#team hi there"
concurrently_
(alice <# "#team bob> hi there")
(cath <# "#team bob> hi there")
cath #> "#team hey team"
concurrently_
(alice <# "#team cath> hey team")
(bob <# "#team cath> hey team")
threadDelay 100000
-- leaving team removes link
alice ##> "/l team"
concurrentlyN_
[ do
alice <## "#team: you left the group"
alice <## "use /d #team to delete the group",
bob <## "#team: alice left the group",
cath <## "#team: alice left the group"
]
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
testGroupLinkDeleteGroupRejoin :: HasCallStack => TestParams -> IO ()
testGroupLinkDeleteGroupRejoin =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
-- use contact so it's not deleted when deleting group
bob <##> alice
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group"
]
bob ##> "/d #team"
bob <## "#team: you deleted the group"
-- re-join via same link
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#team: you joined the group"
]
]
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
testGroupLinkContactUsed :: HasCallStack => TestParams -> IO ()
testGroupLinkContactUsed =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
-- sending/receiving a message marks contact as used
threadDelay 100000
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice #> "@bob hello"
bob <# "alice> hello"
threadDelay 500000
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
alice @@@ [("@bob", ""), ("#team", "connected")]
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
bob @@@ [("@alice", ""), ("#team", "connected")]
testGroupLinkIncognitoMembership :: HasCallStack => TestParams -> IO ()
testGroupLinkIncognitoMembership =
testChatCfg4 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
-- bob connected incognito to alice
alice ##> "/c"
inv <- getInvitation alice
bob ##> ("/c i " <> inv)
bob <## "confirmation sent!"
bobIncognito <- getTermLine bob
concurrentlyN_
[ do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again",
alice <## (bobIncognito <> ": contact is connected")
]
-- alice creates group
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
-- alice invites bob
alice ##> ("/a team " <> bobIncognito <> " admin")
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
bob <## "#team: alice invites you to join the group as admin"
bob <## ("use /j team to join incognito as " <> bobIncognito)
]
bob ##> "/j team"
concurrently_
(alice <## ("#team: " <> bobIncognito <> " joined the group"))
(bob <## ("#team: you joined the group incognito as " <> bobIncognito))
-- bob creates group link, cath joins
bob ##> "/create link #team"
gLink <- getGroupLink bob "team" GRMember True
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
bob <## "cath (Catherine): accepting request to join group #team..."
_ <- getTermLine bob
concurrentlyN_
[ do
bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i cath to print out this incognito profile again"
bob <## "cath invited to group #team via your group link"
bob <## "#team: cath joined the group",
do
cath <## (bobIncognito <> ": contact is connected")
cath <## "#team: you joined the group"
cath <## "#team: member alice (Alice) is connected",
do
alice <## ("#team: " <> bobIncognito <> " added cath (Catherine) to the group (connecting...)")
alice <## "#team: new member cath is connected"
]
bob ?#> "@cath hi, I'm incognito"
cath <# (bobIncognito <> "> hi, I'm incognito")
cath #> ("@" <> bobIncognito <> " hey, I'm cath")
bob ?<# "cath> hey, I'm cath"
-- dan joins incognito
dan ##> ("/c i " <> gLink)
danIncognito <- getTermLine dan
dan <## "connection request sent incognito!"
bob <## (danIncognito <> ": accepting request to join group #team...")
_ <- getTermLine bob
_ <- getTermLine dan
concurrentlyN_
[ do
bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> danIncognito <> " to print out this incognito profile again")
bob <## (danIncognito <> " invited to group #team via your group link")
bob <## ("#team: " <> danIncognito <> " joined the group"),
do
dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito)
dan <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
dan <## ("#team: you joined the group incognito as " <> danIncognito)
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member cath (Catherine) is connected"
],
do
alice <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)")
alice <## ("#team: new member " <> danIncognito <> " is connected"),
do
cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)")
cath <## ("#team: new member " <> danIncognito <> " is connected")
]
bob ?#> ("@" <> danIncognito <> " hi, I'm incognito")
dan ?<# (bobIncognito <> "> hi, I'm incognito")
dan ?#> ("@" <> bobIncognito <> " hey, me too")
bob ?<# (danIncognito <> "> hey, me too")
alice #> "#team hello"
concurrentlyN_
[ bob ?<# "#team alice> hello",
cath <# "#team alice> hello",
dan ?<# "#team alice> hello"
]
bob ?#> "#team hi there"
concurrentlyN_
[ alice <# ("#team " <> bobIncognito <> "> hi there"),
cath <# ("#team " <> bobIncognito <> "> hi there"),
dan ?<# ("#team " <> bobIncognito <> "> hi there")
]
cath #> "#team hey"
concurrentlyN_
[ alice <# "#team cath> hey",
bob ?<# "#team cath> hey",
dan ?<# "#team cath> hey"
]
dan ?#> "#team how is it going?"
concurrentlyN_
[ alice <# ("#team " <> danIncognito <> "> how is it going?"),
bob ?<# ("#team " <> danIncognito <> "> how is it going?"),
cath <# ("#team " <> danIncognito <> "> how is it going?")
]
testGroupLinkUnusedHostContactDeleted :: HasCallStack => TestParams -> IO ()
testGroupLinkUnusedHostContactDeleted =
testChatCfg2 cfg aliceProfile bobProfile $
\alice bob -> do
-- create group 1
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLinkTeam <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLinkTeam)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/create link #club"
gLinkClub <- getGroupLink alice "club" GRMember True
bob ##> ("/c " <> gLinkClub)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #club..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages",
EndsWith "invited to group #club via your group link",
EndsWith "joined the group"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#club: you joined the group"
]
]
-- list contacts
bob ##> "/contacts"
bob <## "alice (Alice)"
-- delete group 1, host contact and profile are kept
bobLeaveDeleteGroup alice bob "team"
bob ##> "/contacts"
bob <## "alice (Alice)"
bob `hasContactProfiles` ["alice", "bob"]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club"
threadDelay 3000000
bob ##> "/contacts"
(bob </)
bob `hasContactProfiles` ["bob"]
where
cfg = mkCfgGroupLinkViaContact $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> IO ()
bobLeaveDeleteGroup alice bob group = do
bob ##> ("/l " <> group)
concurrentlyN_
[ do
bob <## ("#" <> group <> ": you left the group")
bob <## ("use /d #" <> group <> " to delete the group"),
alice <## ("#" <> group <> ": bob left the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => TestParams -> IO ()
testGroupLinkIncognitoUnusedHostContactsDeleted =
testChatCfg2 cfg aliceProfile bobProfile $
\alice bob -> do
bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice"
bobIncognitoClub <- createGroupBobIncognito alice bob "club" "alice_1"
bobIncognitoTeam `shouldNotBe` bobIncognitoClub
-- list contacts
bob ##> "/contacts"
bob <## "i alice (Alice)"
bob <## "i alice_1 (Alice)"
bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub]
-- delete group 1, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam
threadDelay 3000000
bob ##> "/contacts"
bob <## "i alice_1 (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club" bobIncognitoClub
threadDelay 3000000
bob ##> "/contacts"
(bob </)
bob `hasContactProfiles` ["bob"]
where
cfg = mkCfgGroupLinkViaContact $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
createGroupBobIncognito :: HasCallStack => TestCC -> TestCC -> String -> String -> IO String
createGroupBobIncognito alice bob group bobsAliceContact = do
alice ##> ("/g " <> group)
alice <## ("group #" <> group <> " is created")
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
alice ##> ("/create link #" <> group)
gLinkTeam <- getGroupLink alice group GRMember True
bob ##> ("/c i " <> gLinkTeam)
bobIncognito <- getTermLine bob
bob <## "connection request sent incognito!"
alice <## (bobIncognito <> ": accepting request to join group #" <> group <> "...")
_ <- getTermLine bob
concurrentlyN_
[ do
alice <## (bobIncognito <> ": contact is connected")
alice <## (bobIncognito <> " invited to group #" <> group <> " via your group link")
alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"),
do
bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> bobsAliceContact <> " to print out this incognito profile again")
bob <## ("#" <> group <> ": you joined the group incognito as " <> bobIncognito)
]
pure bobIncognito
bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> String -> IO ()
bobLeaveDeleteGroup alice bob group bobIncognito = do
bob ##> ("/l " <> group)
concurrentlyN_
[ do
bob <## ("#" <> group <> ": you left the group")
bob <## ("use /d #" <> group <> " to delete the group"),
alice <## ("#" <> group <> ": " <> bobIncognito <> " left the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
testGroupLinkMemberRole :: HasCallStack => TestParams -> IO ()
testGroupLinkMemberRole =
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
\alice bob cath -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team admin"
alice <## "#team: initial role for group member cannot be admin, use member or observer"
alice ##> "/create link #team observer"
gLink <- getGroupLink alice "team" GRObserver True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
alice ##> "/set link role #team admin"
alice <## "#team: initial role for group member cannot be admin, use member or observer"
alice ##> "/set link role #team member"
_ <- getGroupLink alice "team" GRMember False
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
alice <## "cath (Catherine): accepting request to join group #team..."
-- if contact existed it is merged
concurrentlyN_
[ alice
<### [ "cath (Catherine): contact is connected",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
cath
<### [ "alice (Alice): contact is connected",
"#team: you joined the group",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
cath #> "#team hello too"
concurrently_
(alice <# "#team cath> hello too")
(bob <# "#team cath> hello too")
bob ##> "#team hey"
bob <## "#team: you don't have permission to send messages"
alice ##> "/mr #team bob member"
alice <## "#team: you changed the role of bob from observer to member"
concurrently_
(bob <## "#team: alice changed your role from observer to member")
(cath <## "#team: alice changed the role of bob from observer to member")
bob #> "#team hey now"
concurrently_
(alice <# "#team bob> hey now")
(cath <# "#team bob> hey now")
testGroupLinkLeaveDelete :: HasCallStack => TestParams -> IO ()
testGroupLinkLeaveDelete =
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers cath bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#team: you joined the group"
]
]
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
alice <## "cath (Catherine): accepting request to join group #team..."
concurrentlyN_
[ alice
<### [ "cath (Catherine): contact is connected",
"cath invited to group #team via your group link",
"#team: cath joined the group"
],
cath
<### [ "alice (Alice): contact is connected",
"#team: you joined the group",
"#team: member bob_1 (Bob) is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages"
],
bob
<### [ "#team: alice added cath_1 (Catherine) to the group (connecting...)",
"#team: new member cath_1 is connected",
"contact cath_1 is merged into cath",
"use @cath <message> to send messages"
]
]
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group",
cath <## "#team: bob left the group"
]
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
bob ##> "/d #team"
bob <## "#team: you deleted the group"
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
testPlanGroupLinkOkKnown :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkOkKnown =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: ok to connect"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
testPlanHostContactDeletedGroupLinkKnown :: HasCallStack => TestParams -> IO ()
testPlanHostContactDeletedGroupLinkKnown =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
alice <##> bob
threadDelay 500000
bob ##> "/d alice"
bob <## "alice: contact is deleted"
alice <## "bob (Bob) deleted contact with you"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
testPlanGroupLinkOwn :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkOwn ps =
withNewTestChatCfg ps (mkCfgGroupLinkViaContact testCfgSlow) "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
alice ##> ("/_connect plan 1 " <> gLink)
alice <## "group link: own link for group #team"
let gLinkSchema2 = linkAnotherSchema gLink
alice ##> ("/_connect plan 1 " <> gLinkSchema2)
alice <## "group link: own link for group #team"
alice ##> ("/c " <> gLink)
alice <## "connection request sent!"
alice <## "alice_1 (Alice): accepting request to join group #team..."
alice
<### [ "alice_1 (Alice): contact is connected",
"alice_1 invited to group #team via your group link",
"#team: alice_1 joined the group",
"alice_2 (Alice): contact is connected",
"#team_1: you joined the group",
"contact alice_2 is merged into alice_1",
"use @alice_1 <message> to send messages"
]
alice `send` "#team 1"
alice
<### [ WithTime "#team 1",
WithTime "#team_1 alice_1> 1"
]
alice `send` "#team_1 2"
alice
<### [ WithTime "#team_1 2",
WithTime "#team alice_1> 2"
]
alice ##> ("/_connect plan 1 " <> gLink)
alice <## "group link: own link for group #team"
alice ##> ("/_connect plan 1 " <> gLinkSchema2)
alice <## "group link: own link for group #team"
-- group works if merged contact is deleted
alice ##> "/d alice_1"
alice <## "alice_1: contact is deleted"
alice `send` "#team 3"
alice
<### [ WithTime "#team 3",
WithTime "#team_1 alice_1> 3"
]
alice `send` "#team_1 4"
alice
<### [ WithTime "#team_1 4",
WithTime "#team alice_1> 4"
]
testPlanGroupLinkConnecting :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkConnecting ps = do
-- gLink <- withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
gLink <- withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
getGroupLink alice "team" GRMember True
-- withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
threadDelay 100000
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting, allowed to reconnect"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting, allowed to reconnect"
threadDelay 100000
-- withTestChatCfg ps cfg "alice" $ \alice -> do
withTestChatCfg ps cfg "alice" $ \alice -> do
alice
<### [ "1 group links active",
"#team: group is empty",
"bob (Bob): accepting request to join group #team..."
]
-- withTestChatCfg ps cfg "bob" $ \bob -> do
withTestChatCfg ps cfg "bob" $ \bob -> do
threadDelay 500000
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting"
bob ##> ("/c " <> gLink)
bob <## "group link: connecting"
where
cfg = mkCfgGroupLinkViaContact testCfgSlow
testPlanGroupLinkLeaveRejoin :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkLeaveRejoin =
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
threadDelay 100000
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
threadDelay 100000
bob ##> "/leave #team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group"
]
threadDelay 100000
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: ok to connect"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: ok to connect"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"#team_1: you joined the group",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages"
]
]
alice #> "#team hi"
bob <# "#team_1 alice> hi"
bob #> "#team_1 hey"
alice <# "#team bob> hey"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team_1"
bob <## "use #team_1 <message> to send messages"
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: known group #team_1"
bob <## "use #team_1 <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team_1"
bob <## "use #team_1 <message> to send messages"
testGroupLinkNoContact :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/set history #team off"
alice <## "updated group preferences:"
alice <## "Recent history: off"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")])
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice ##> "/contacts"
bob ##> "/contacts"
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
concurrentlyN_
[ do
alice <## "cath (Catherine): accepting request to join group #team..."
alice <## "#team: cath joined the group",
do
cath <## "#team: joining the group..."
cath <## "#team: you joined the group"
cath <## "#team: member bob (Bob) is connected",
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath #> "#team hey"
alice <# "#team cath> hey"
bob <# "#team cath> hey"
bob #> "#team hi cath"
alice <# "#team bob> hi cath"
cath <# "#team bob> hi cath"
testGroupLinkNoContactInviteesWereConnected :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactInviteesWereConnected =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers bob cath
bob <##> cath
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/set history #team off"
alice <## "updated group preferences:"
alice <## "Recent history: off"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")])
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected"), ("@cath", "hey")]
alice ##> "/contacts"
bob ##> "/contacts"
bob <## "cath (Catherine)"
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
concurrentlyN_
[ do
alice <## "cath (Catherine): accepting request to join group #team..."
alice <## "#team: cath joined the group",
cath
<### [ "#team: joining the group...",
"#team: you joined the group",
"#team: member bob_1 (Bob) is connected",
"contact and member are merged: bob, #team bob_1",
"use @bob <message> to send messages"
],
bob
<### [ "#team: alice added cath_1 (Catherine) to the group (connecting...)",
"#team: new member cath_1 is connected",
"contact and member are merged: cath, #team cath_1",
"use @cath <message> to send messages"
]
]
-- message delivery works
bob <##> cath
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testGroupLinkNoContactAllMembersWereConnected :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactAllMembersWereConnected =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
alice <##> bob
connectUsers alice cath
alice <##> cath
connectUsers bob cath
bob <##> cath
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/set history #team off"
alice <## "updated group preferences:"
alice <## "Recent history: off"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "#team: bob_1 joined the group"
alice <## "contact and member are merged: bob, #team bob_1"
alice <## "use @bob <message> to send messages",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
bob <## "contact and member are merged: alice, #team alice_1"
bob <## "use @alice <message> to send messages"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")])
alice @@@ [("#team", "connected"), ("@bob", "hey"), ("@cath", "hey")]
bob @@@ [("#team", "connected"), ("@alice", "hey"), ("@cath", "hey")]
alice ##> "/contacts"
alice <## "bob (Bob)"
alice <## "cath (Catherine)"
bob ##> "/contacts"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
concurrentlyN_
[ alice
<### [ "cath_1 (Catherine): accepting request to join group #team...",
"#team: cath_1 joined the group",
"contact and member are merged: cath, #team cath_1",
"use @cath <message> to send messages"
],
cath
<### [ "#team: joining the group...",
"#team: you joined the group",
"#team: member bob_1 (Bob) is connected",
"contact and member are merged: bob, #team bob_1",
"use @bob <message> to send messages",
"contact and member are merged: alice, #team alice_1",
"use @alice <message> to send messages"
],
bob
<### [ "#team: alice added cath_1 (Catherine) to the group (connecting...)",
"#team: new member cath_1 is connected",
"contact and member are merged: cath, #team cath_1",
"use @cath <message> to send messages"
]
]
-- message delivery works
alice <##> bob
alice <##> cath
bob <##> cath
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testGroupLinkNoContactMemberRole :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactMemberRole =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team observer"
gLink <- getGroupLink alice "team" GRObserver True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
threadDelay 100000
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): observer, invited, connected"
]
bob ##> "/ms team"
bob
<### [ "alice (Alice): owner, host, connected",
"bob (Bob): observer, you, connected"
]
bob ##> "#team hi there"
bob <## "#team: you don't have permission to send messages"
alice ##> "/mr #team bob member"
alice <## "#team: you changed the role of bob from observer to member"
bob <## "#team: alice changed your role from observer to member"
bob #> "#team hey now"
alice <# "#team bob> hey now"
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
concurrentlyN_
[ do
alice <## "cath (Catherine): accepting request to join group #team..."
alice <## "#team: cath joined the group",
cath
<### [ "#team: joining the group...",
"#team: you joined the group",
WithTime "#team bob> hey now [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
bob #> "#team hi cath"
alice <# "#team bob> hi cath"
cath <# "#team bob> hi cath"
cath ##> "#team hey"
cath <## "#team: you don't have permission to send messages"
alice ##> "/mr #team cath admin"
alice <## "#team: you changed the role of cath from observer to admin"
cath <## "#team: alice changed your role from observer to admin"
bob <## "#team: alice changed the role of cath from observer to admin"
cath #> "#team hey"
alice <# "#team cath> hey"
bob <# "#team cath> hey"
cath ##> "/mr #team bob admin"
cath <## "#team: you changed the role of bob from member to admin"
bob <## "#team: cath changed your role from member to admin"
alice <## "#team: cath changed the role of bob from member to admin"
testGroupLinkNoContactHostIncognito :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactHostIncognito =
testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/g i team"
aliceIncognito <- getTermLine alice
alice <## ("group #team is created, your incognito profile for this group is " <> aliceIncognito)
alice <## "to add members use /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")])
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice ##> "/contacts"
bob ##> "/contacts"
alice ?#> "#team hello"
bob <# ("#team " <> aliceIncognito <> "> hello")
bob #> "#team hi there"
alice ?<# "#team bob> hi there"
testGroupLinkNoContactInviteeIncognito :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactInviteeIncognito =
testChat2 aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c i " <> gLink)
bobIncognito <- getTermLine bob
bob <## "connection request sent incognito!"
alice <## (bobIncognito <> ": accepting request to join group #team...")
concurrentlyN_
[ alice <## ("#team: " <> bobIncognito <> " joined the group"),
do
bob <## "#team: joining the group..."
bob <## ("#team: you joined the group incognito as " <> bobIncognito)
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")])
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice ##> "/contacts"
bob ##> "/contacts"
alice #> "#team hello"
bob ?<# "#team alice> hello"
bob ?#> "#team hi there"
alice <# ("#team " <> bobIncognito <> "> hi there")
testGroupLinkNoContactHostProfileReceived :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactHostProfileReceived =
testChat2 aliceProfile bobProfile $
\alice bob -> do
let profileImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="
alice ##> ("/set profile image " <> profileImage)
alice <## "profile image updated"
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
threadDelay 100000
aliceImage <- getProfilePictureByName bob "alice"
aliceImage `shouldBe` Just profileImage
testGroupLinkNoContactExistingContactMerged :: HasCallStack => TestParams -> IO ()
testGroupLinkNoContactExistingContactMerged =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "#team: bob_1 joined the group"
alice <## "contact and member are merged: bob, #team bob_1"
alice <## "use @bob <message> to send messages",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
bob <## "contact and member are merged: alice, #team alice_1"
bob <## "use @alice <message> to send messages"
]
threadDelay 100000
alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")])
alice <##> bob
alice @@@ [("#team", "connected"), ("@bob", "hey")]
bob @@@ [("#team", "connected"), ("@alice", "hey")]
alice ##> "/contacts"
alice <## "bob (Bob)"
bob ##> "/contacts"
bob <## "alice (Alice)"
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
testPlanGroupLinkNoContactKnown :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkNoContactKnown =
testChat2 aliceProfile bobProfile $
\alice bob -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: ok to connect"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
testPlanGroupLinkNoContactConnecting :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkNoContactConnecting ps = do
gLink <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
getGroupLink alice "team" GRMember True
withNewTestChat ps "bob" bobProfile $ \bob -> do
threadDelay 100000
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting, allowed to reconnect"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting, allowed to reconnect"
threadDelay 100000
withTestChat ps "alice" $ \alice -> do
alice
<### [ "1 group links active",
"#team: group is empty",
"bob (Bob): accepting request to join group #team..."
]
withTestChat ps "bob" $ \bob -> do
threadDelay 500000
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/c " <> gLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
testPlanGroupLinkNoContactConnectingSlow :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkNoContactConnectingSlow ps = do
gLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
getGroupLink alice "team" GRMember True
withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do
threadDelay 100000
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting, allowed to reconnect"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting, allowed to reconnect"
threadDelay 100000
withTestChatCfg ps testCfgSlow "alice" $ \alice -> do
alice
<### [ "1 group links active",
"#team: group is empty",
"bob (Bob): accepting request to join group #team..."
]
withTestChatCfg ps testCfgSlow "bob" $ \bob -> do
threadDelay 500000
bob <## "#team: joining the group..."
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting to group #team"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting to group #team"
bob ##> ("/c " <> gLink)
bob <## "group link: connecting to group #team"
#if !defined(dbPostgres)
testGroupMsgDecryptError :: HasCallStack => TestParams -> IO ()
testGroupMsgDecryptError ps =
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
setupDesynchronizedRatchet ps alice
withTestChat ps "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
alice #> "#team hello again"
bob <# "#team alice> skipped message ID 9..11"
bob <# "#team alice> hello again"
bob #> "#team received!"
alice <# "#team bob> received!"
setupDesynchronizedRatchet :: HasCallStack => TestParams -> TestCC -> IO ()
setupDesynchronizedRatchet ps alice = do
copyDb "bob" "bob_old"
withTestChat ps "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
alice #> "#team 1"
bob <# "#team alice> 1"
bob #> "#team 2"
alice <# "#team bob> 2"
alice #> "#team 3"
bob <# "#team alice> 3"
bob #> "#team 4"
alice <# "#team bob> 4"
withTestChat ps "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob ##> "/sync #team alice"
bob <## "error: command is prohibited, synchronizeRatchet: not allowed"
alice #> "#team 1"
bob <## "#team alice: decryption error (connection out of sync), synchronization required"
bob <## "use /sync #team alice to synchronize"
alice #> "#team 2"
alice #> "#team 3"
(bob </)
bob ##> "/tail #team 1"
bob <# "#team alice> decryption error, possibly due to the device change (header, 3 messages)"
where
tmp = tmpPath ps
copyDb from to = do
copyFile (tmp </> (from <> chatSuffix)) (tmp </> (to <> chatSuffix))
copyFile (tmp </> (from <> agentSuffix)) (tmp </> (to <> agentSuffix))
testGroupSyncRatchet :: HasCallStack => TestParams -> IO ()
testGroupSyncRatchet ps =
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
setupDesynchronizedRatchet ps alice
withTestChat ps "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob `send` "#team 1"
-- "send prohibited" error is not printed in group as SndMessage is created,
-- but it should be displayed in per member snd statuses
bob <# "#team 1"
(alice </)
-- synchronize bob and alice
bob ##> "/sync #team alice"
bob <## "connection synchronization started"
alice <## "#team bob: connection synchronization agreed"
bob <## "#team alice: connection synchronization agreed"
alice <## "#team bob: connection synchronized"
bob <## "#team alice: connection synchronized"
threadDelay 100000
bob #$> ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")])
alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
alice #> "#team hello again"
bob <# "#team alice> hello again"
bob #> "#team received!"
alice <# "#team bob> received!"
testGroupSyncRatchetCodeReset :: HasCallStack => TestParams -> IO ()
testGroupSyncRatchetCodeReset ps =
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
-- connection not verified
bob ##> "/i #team alice"
aliceInfo bob False
-- verify connection
alice ##> "/code #team bob"
bCode <- getTermLine alice
bob ##> ("/verify #team alice " <> bCode)
bob <## "connection verified"
-- connection verified
bob ##> "/i #team alice"
aliceInfo bob True
setupDesynchronizedRatchet ps alice
withTestChat ps "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob ##> "/sync #team alice"
bob <## "connection synchronization started"
alice <## "#team bob: connection synchronization agreed"
bob <## "#team alice: connection synchronization agreed"
bob <## "#team alice: security code changed"
alice <## "#team bob: connection synchronized"
bob <## "#team alice: connection synchronized"
threadDelay 100000
bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")])
alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")])
-- connection not verified
bob ##> "/i #team alice"
aliceInfo bob False
alice #> "#team hello again"
bob <# "#team alice> hello again"
bob #> "#team received!"
alice <# "#team bob> received!"
where
aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
aliceInfo bob verified = do
bob <## "group ID: 1"
bob <## "member ID: 1"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## connVerified
bob <## currentChatVRangeInfo
where
connVerified
| verified = "connection verified"
| otherwise = "connection not verified, use /code command to see security code"
#endif
testSetGroupMessageReactions :: HasCallStack => TestParams -> IO ()
testSetGroupMessageReactions =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hi"
bob <# "#team alice> hi"
cath <# "#team alice> hi"
bob ##> "+1 #team hi"
bob <## "added 👍"
alice <# "#team bob> > alice hi"
alice <## " + 👍"
cath <# "#team bob> > alice hi"
cath <## " + 👍"
bob ##> "+1 #team hi"
bob <## "bad chat command: reaction already added"
bob ##> "+^ #team hi"
bob <## "added 🚀"
alice <# "#team bob> > alice hi"
alice <## " + 🚀"
cath <# "#team bob> > alice hi"
cath <## " + 🚀"
alice ##> "/tail #team 1"
alice <# "#team hi"
alice <## " 👍 1 🚀 1"
bob ##> "/tail #team 1"
bob <# "#team alice> hi"
bob <## " 👍 1 🚀 1"
bob ##> "/tail #team 1"
bob <# "#team alice> hi"
bob <## " 👍 1 🚀 1"
alice ##> "+1 #team hi"
alice <## "added 👍"
bob <# "#team alice> > alice hi"
bob <## " + 👍"
cath <# "#team alice> > alice hi"
cath <## " + 👍"
alice ##> "/tail #team 1"
alice <# "#team hi"
alice <## " 👍 2 🚀 1"
bob ##> "/tail #team 1"
bob <# "#team alice> hi"
bob <## " 👍 2 🚀 1"
cath ##> "/tail #team 1"
cath <# "#team alice> hi"
cath <## " 👍 2 🚀 1"
itemId' <- lastItemId alice
alice ##> ("/_reaction members 1 #1 " <> itemId' <> " {\"type\": \"emoji\", \"emoji\": \"👍\"}")
alice <## "2 member(s) reacted"
bob ##> "-1 #team hi"
bob <## "removed 👍"
alice <# "#team bob> > alice hi"
alice <## " - 👍"
cath <# "#team bob> > alice hi"
cath <## " - 👍"
bob ##> "-^ #team hi"
bob <## "removed 🚀"
alice <# "#team bob> > alice hi"
alice <## " - 🚀"
cath <# "#team bob> > alice hi"
cath <## " - 🚀"
alice ##> "/tail #team 1"
alice <# "#team hi"
alice <## " 👍 1"
bob ##> "/tail #team 1"
bob <# "#team alice> hi"
bob <## " 👍 1"
cath ##> "/tail #team 1"
cath <# "#team alice> hi"
cath <## " 👍 1"
testSendGroupDeliveryReceipts :: HasCallStack => TestParams -> IO ()
testSendGroupDeliveryReceipts ps =
withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
-- turn off contacts receipts for tests
alice ##> "/_set receipts contacts 1 off"
alice <## "ok"
bob ##> "/_set receipts contacts 1 off"
bob <## "ok"
cath ##> "/_set receipts contacts 1 off"
cath <## "ok"
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hi"
bob <# "#team alice> hi"
cath <# "#team alice> hi"
alice % "#team hi"
alice "#team hi"
bob #> "#team hey"
alice <# "#team bob> hey"
cath <# "#team bob> hey"
bob % "#team hey"
bob "#team hey"
where
cfg = testCfg {showReceipts = True}
testConfigureGroupDeliveryReceipts :: HasCallStack => TestParams -> IO ()
testConfigureGroupDeliveryReceipts ps =
withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do
withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do
-- turn off contacts receipts for tests
alice ##> "/_set receipts contacts 1 off"
alice <## "ok"
bob ##> "/_set receipts contacts 1 off"
bob <## "ok"
cath ##> "/_set receipts contacts 1 off"
cath <## "ok"
-- create group 1
createGroup3 "team" alice bob cath
threadDelay 1000000
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as member"
bob <## "use /j club to accept"
]
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
alice ##> "/a club cath"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as member"
cath <## "use /j club to accept"
]
cath ##> "/j club"
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: you joined the group"
cath <## "#club: member bob_1 (Bob) is connected"
cath <## "contact bob_1 is merged into bob"
cath <## "use @bob <message> to send messages",
do
bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)"
bob <## "#club: new member cath_1 is connected"
bob <## "contact cath_1 is merged into cath"
bob <## "use @cath <message> to send messages"
]
threadDelay 1000000
-- for new users receipts are enabled by default
receipt bob alice cath "team" "1"
receipt bob alice cath "club" "2"
-- configure receipts in all chats
alice ##> "/set receipts all off"
alice <## "ok"
partialReceipt bob alice cath "team" "3"
partialReceipt bob alice cath "club" "4"
-- configure receipts for user groups
alice ##> "/_set receipts groups 1 on"
alice <## "ok"
receipt bob alice cath "team" "5"
receipt bob alice cath "club" "6"
-- configure receipts for user groups (terminal api)
alice ##> "/set receipts groups off"
alice <## "ok"
partialReceipt bob alice cath "team" "7"
partialReceipt bob alice cath "club" "8"
-- configure receipts for group
alice ##> "/receipts #team on"
alice <## "ok"
receipt bob alice cath "team" "9"
partialReceipt bob alice cath "club" "10"
-- configure receipts for user groups (don't clear overrides)
alice ##> "/_set receipts groups 1 off"
alice <## "ok"
receipt bob alice cath "team" "11"
partialReceipt bob alice cath "club" "12"
alice ##> "/_set receipts groups 1 off clear_overrides=off"
alice <## "ok"
receipt bob alice cath "team" "13"
partialReceipt bob alice cath "club" "14"
-- configure receipts for user groups (clear overrides)
alice ##> "/set receipts groups off clear_overrides=on"
alice <## "ok"
partialReceipt bob alice cath "team" "15"
partialReceipt bob alice cath "club" "16"
-- configure receipts for group, reset to default
alice ##> "/receipts #team on"
alice <## "ok"
receipt bob alice cath "team" "17"
partialReceipt bob alice cath "club" "18"
alice ##> "/receipts #team default"
alice <## "ok"
partialReceipt bob alice cath "team" "19"
partialReceipt bob alice cath "club" "20"
-- cath - disable receipts for user groups
cath ##> "/_set receipts groups 1 off"
cath <## "ok"
noReceipt bob alice cath "team" "21"
noReceipt bob alice cath "club" "22"
-- partial, all receipts in one group; no receipts in other group
cath ##> "/receipts #team on"
cath <## "ok"
partialReceipt bob alice cath "team" "23"
noReceipt bob alice cath "club" "24"
alice ##> "/receipts #team on"
alice <## "ok"
receipt bob alice cath "team" "25"
noReceipt bob alice cath "club" "26"
where
cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True}
receipt cc1 cc2 cc3 gName msg = do
name1 <- userName cc1
cc1 #> ("#" <> gName <> " " <> msg)
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc1 % ("#" <> gName <> " " <> msg)
cc1 ("#" <> gName <> " " <> msg)
partialReceipt cc1 cc2 cc3 gName msg = do
name1 <- userName cc1
cc1 #> ("#" <> gName <> " " <> msg)
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc1 % ("#" <> gName <> " " <> msg)
noReceipt cc1 cc2 cc3 gName msg = do
name1 <- userName cc1
cc1 #> ("#" <> gName <> " " <> msg)
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc1 <// 50000
testNoGroupDirectConns :: HasCallStack => VersionRangeChat -> VersionRangeChat -> VersionRangeChat -> Bool -> TestParams -> IO ()
testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns ps =
withNewTestChatCfg ps testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do
withNewTestChatCfg ps testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do
withNewTestChatCfg ps testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do
createGroup3 "team" alice bob cath
if noDirectConns
then contactsDontExist bob cath
else contactsExist bob cath
where
contactsDontExist bob cath = do
bob ##> "/contacts"
bob <## "alice (Alice)"
cath ##> "/contacts"
cath <## "alice (Alice)"
contactsExist bob cath = do
bob ##> "/contacts"
bob
<### [ "alice (Alice)",
"cath (Catherine)"
]
cath ##> "/contacts"
cath
<### [ "alice (Alice)",
"bob (Bob)"
]
bob <##> cath
testNoDirectDifferentLDNs :: HasCallStack => TestParams -> IO ()
testNoDirectDifferentLDNs =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
addMember "club" alice bob GRAdmin
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
addMember "club" alice cath GRAdmin
cath ##> "/j club"
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: you joined the group"
cath <## "#club: member bob_1 (Bob) is connected",
do
bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)"
bob <## "#club: new member cath_1 is connected"
]
testGroupLDNs alice bob cath "team" "bob" "cath"
testGroupLDNs alice bob cath "club" "bob_1" "cath_1"
alice `hasContactProfiles` ["alice", "bob", "cath"]
bob `hasContactProfiles` ["bob", "alice", "cath", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
where
testGroupLDNs alice bob cath gName bobLDN cathLDN = do
alice ##> ("/ms " <> gName)
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): admin, invited, connected",
"cath (Catherine): admin, invited, connected"
]
bob ##> ("/ms " <> gName)
bob
<### [ "alice (Alice): owner, host, connected",
"bob (Bob): admin, you, connected",
ConsoleString (cathLDN <> " (Catherine): admin, connected")
]
cath ##> ("/ms " <> gName)
cath
<### [ "alice (Alice): owner, host, connected",
ConsoleString (bobLDN <> " (Bob): admin, connected"),
"cath (Catherine): admin, you, connected"
]
alice #> ("#" <> gName <> " hello")
concurrentlyN_
[ bob <# ("#" <> gName <> " alice> hello"),
cath <# ("#" <> gName <> " alice> hello")
]
bob #> ("#" <> gName <> " hi there")
concurrentlyN_
[ alice <# ("#" <> gName <> " bob> hi there"),
cath <# ("#" <> gName <> " " <> bobLDN <> "> hi there")
]
cath #> ("#" <> gName <> " hey")
concurrentlyN_
[ alice <# ("#" <> gName <> " cath> hey"),
bob <# ("#" <> gName <> " " <> cathLDN <> "> hey")
]
testMergeMemberExistingContact :: HasCallStack => TestParams -> IO ()
testMergeMemberExistingContact =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers alice cath
createGroup2 "team" bob cath
bob ##> "/a #team alice"
bob <## "invitation to join the group #team sent to alice"
alice <## "#team: bob invites you to join the group as member"
alice <## "use /j team to accept"
alice ##> "/j team"
concurrentlyN_
[ do
alice <## "#team: you joined the group"
alice <## "#team: member cath_1 (Catherine) is connected"
alice <## "contact and member are merged: cath, #team cath_1"
alice <## "use @cath <message> to send messages",
do
bob <## "#team: alice joined the group",
do
cath <## "#team: bob added alice_1 (Alice) to the group (connecting...)"
cath <## "#team: new member alice_1 is connected"
cath <## "contact and member are merged: alice, #team alice_1"
cath <## "use @alice <message> to send messages"
]
alice <##> cath
alice #> "#team hello"
bob <# "#team alice> hello"
cath <# "#team alice> hello"
cath #> "#team hello too"
bob <# "#team cath> hello too"
alice <# "#team cath> hello too"
alice ##> "/contacts"
alice
<### [ "bob (Bob)",
"cath (Catherine)"
]
cath ##> "/contacts"
cath
<### [ "alice (Alice)",
"bob (Bob)"
]
alice `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
testMergeContactExistingMember :: HasCallStack => TestParams -> IO ()
testMergeContactExistingMember =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
bob ##> "/c"
inv' <- getInvitation bob
cath ##> ("/c " <> inv')
cath <## "confirmation sent!"
concurrentlyN_
[ bob
<### [ "cath_1 (Catherine): contact is connected",
"contact and member are merged: cath_1, #team cath",
"use @cath <message> to send messages"
],
cath
<### [ "bob_1 (Bob): contact is connected",
"contact and member are merged: bob_1, #team bob",
"use @bob <message> to send messages"
]
]
bob <##> cath
bob ##> "/contacts"
bob <### ["alice (Alice)", "cath (Catherine)"]
cath ##> "/contacts"
cath <### ["alice (Alice)", "bob (Bob)"]
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
testMergeContactMultipleMembers :: HasCallStack => TestParams -> IO ()
testMergeContactMultipleMembers =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
create2Groups3 "team" "club" alice bob cath
bob `hasContactProfiles` ["alice", "bob", "cath", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob", "bob"]
bob ##> "/c"
inv' <- getInvitation bob
cath ##> ("/c " <> inv')
cath <## "confirmation sent!"
concurrentlyN_
[ bob
<### [ "cath_2 (Catherine): contact is connected",
StartsWith "contact and member are merged: cath",
StartsWith "use @cath",
StartsWith "contact and member are merged: cath",
StartsWith "use @cath"
],
cath
<### [ "bob_2 (Bob): contact is connected",
StartsWith "contact and member are merged: bob",
StartsWith "use @bob",
StartsWith "contact and member are merged: bob",
StartsWith "use @bob"
]
]
bob <##> cath
bob ##> "/contacts"
bob <### ["alice (Alice)", "cath (Catherine)"]
cath ##> "/contacts"
cath <### ["alice (Alice)", "bob (Bob)"]
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath `hasContactProfiles` ["cath", "alice", "bob"]
testMergeGroupLinkHostMultipleContacts :: HasCallStack => TestParams -> IO ()
testMergeGroupLinkHostMultipleContacts =
testChatCfg2 testCfgGroupLinkViaContact bobProfile cathProfile $
\bob cath -> do
connectUsers bob cath
bob ##> "/c"
inv' <- getInvitation bob
cath ##> ("/c " <> inv')
cath <## "confirmation sent!"
concurrently_
(bob <## "cath_1 (Catherine): contact is connected")
(cath <## "bob_1 (Bob): contact is connected")
bob `hasContactProfiles` ["bob", "cath", "cath"]
cath `hasContactProfiles` ["cath", "bob", "bob"]
bob ##> "/g party"
bob <## "group #party is created"
bob <## "to add members use /a party <name> or /create link #party"
bob ##> "/create link #party"
gLink <- getGroupLink bob "party" GRMember True
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
bob <## "cath_2 (Catherine): accepting request to join group #party..."
concurrentlyN_
[ bob
<### [ "cath_2 (Catherine): contact is connected",
EndsWith "invited to group #party via your group link",
EndsWith "joined the group",
StartsWith "contact cath_2 is merged into cath",
StartsWith "use @cath"
],
cath
<### [ "bob_2 (Bob): contact is connected",
"#party: you joined the group",
StartsWith "contact bob_2 is merged into bob",
StartsWith "use @bob"
]
]
bob <##> cath
bob ##> "/contacts"
bob <### ["cath (Catherine)", "cath_1 (Catherine)"]
cath ##> "/contacts"
cath <### ["bob (Bob)", "bob_1 (Bob)"]
bob `hasContactProfiles` ["bob", "cath", "cath"]
cath `hasContactProfiles` ["cath", "bob", "bob"]
testMemberContactMessage :: HasCallStack => TestParams -> IO ()
testMemberContactMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
-- alice and bob delete contacts, connect
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
bob ##> "/d alice"
bob <## "alice: contact is deleted"
alice ##> "@#team bob hi"
alice
<### [ "member #team bob does not have direct connection, creating",
"contact for member #team bob is created",
"sent invitation to connect directly to member #team bob",
WithTime "@bob hi"
]
bob
<### [ "#team alice is creating direct contact alice with you",
WithTime "alice> hi"
]
bob <## "alice (Alice): you can send messages to contact"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
bob #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")])
-- exchanging messages will enable PQ (see Chat "TODO PQ" - perhaps connection should be negotiated with PQ on)
alice <##> bob
alice <##> bob
alice `send` "@bob hi"
alice <## "bob: quantum resistant end-to-end encryption enabled"
alice <# "@bob hi"
bob <## "alice: quantum resistant end-to-end encryption enabled"
bob <# "alice> hi"
bob #> "@alice hey"
alice <# "bob> hey"
alice <##> bob
-- bob and cath connect
bob ##> "@#team cath hi"
bob
<### [ "member #team cath does not have direct connection, creating",
"contact for member #team cath is created",
"sent invitation to connect directly to member #team cath",
WithTime "@cath hi"
]
cath
<### [ "#team bob is creating direct contact bob with you",
WithTime "bob> hi"
]
cath <## "bob (Bob): you can send messages to contact"
concurrently_
(bob <## "cath (Catherine): contact is connected")
(cath <## "bob (Bob): contact is connected")
cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")])
bob <##> cath
testMemberContactNoMessage :: HasCallStack => TestParams -> IO ()
testMemberContactNoMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
-- bob and cath connect
bob ##> "/_create member contact #1 3"
bob <## "contact for member #team cath is created"
bob ##> "/_invite member contact @3"
bob <## "sent invitation to connect directly to member #team cath"
cath <## "#team bob is creating direct contact bob with you"
cath <## "bob (Bob): you can send messages to contact"
concurrently_
(bob <## "cath (Catherine): contact is connected")
(cath <## "bob (Bob): contact is connected")
cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")])
bob <##> cath
testMemberContactProhibitedContactExists :: HasCallStack => TestParams -> IO ()
testMemberContactProhibitedContactExists =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/_create member contact #1 2"
alice <## "bad chat command: member contact already exists"
alice ##> "@#team bob hi"
alice <# "@bob hi"
bob <# "alice> hi"
testMemberContactProhibitedRepeatInv :: HasCallStack => TestParams -> IO ()
testMemberContactProhibitedRepeatInv =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
bob ##> "/_create member contact #1 3"
bob <## "contact for member #team cath is created"
bob ##> "/_invite member contact @3 text hi"
bob
<### [ "sent invitation to connect directly to member #team cath",
WithTime "@cath hi"
]
bob ##> "/_invite member contact @3 text hey"
bob <## "bad chat command: x.grp.direct.inv already sent"
cath
<### [ "#team bob is creating direct contact bob with you",
WithTime "bob> hi"
]
cath <## "bob (Bob): you can send messages to contact"
concurrently_
(bob <## "cath (Catherine): contact is connected")
(cath <## "bob (Bob): contact is connected")
bob <##> cath
testMemberContactInvitedConnectionReplaced :: HasCallStack => TestParams -> IO ()
testMemberContactInvitedConnectionReplaced ps = do
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "cath" cathProfile $ \cath -> do
createGroup3 "team" alice bob cath
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "@#team bob hi"
alice
<### [ "member #team bob does not have direct connection, creating",
"contact for member #team bob is created",
"sent invitation to connect directly to member #team bob",
WithTime "@bob hi"
]
bob
<### [ "#team alice is creating direct contact alice with you",
WithTime "alice> hi",
"alice: security code changed"
]
bob <## "alice (Alice): you can send messages to contact"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
bob ##> "/_get chat @2 count=100"
items <- chat <$> getTermLine bob
items `shouldContain` [(0, "security code changed")]
withTestChat ps "bob" $ \bob -> do
subscriptions bob 1
checkConnectionsWork alice bob
withTestChat ps "alice" $ \alice -> do
subscriptions alice 2
withTestChat ps "bob" $ \bob -> do
subscriptions bob 1
checkConnectionsWork alice bob
withTestChat ps "cath" $ \cath -> do
subscriptions cath 1
-- group messages work
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath <# "#team alice> hello")
bob #> "#team hi there"
concurrently_
(alice <# "#team bob> hi there")
(cath <# "#team bob> hi there")
cath #> "#team hey team"
concurrently_
(alice <# "#team cath> hey team")
(bob <# "#team cath> hey team")
where
subscriptions :: TestCC -> Int -> IO ()
subscriptions cc n = do
cc <## (show n <> " contacts connected (use /cs for the list)")
cc <## "#team: connected to server(s)"
checkConnectionsWork alice bob = do
alice <##> bob
alice @@@ [("@bob", "hey"), ("@cath", "sent invitation to join group team as admin"), ("#team", "connected")]
bob @@@ [("@alice", "hey"), ("#team", "started direct connection with you")]
testMemberContactIncognito :: HasCallStack => TestParams -> IO ()
testMemberContactIncognito =
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
\alice bob cath -> do
-- create group, bob joins incognito
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c i " <> gLink)
bobIncognito <- getTermLine bob
bob <## "connection request sent incognito!"
alice <## (bobIncognito <> ": accepting request to join group #team...")
_ <- getTermLine bob
concurrentlyN_
[ do
alice <## (bobIncognito <> ": contact is connected")
alice <## (bobIncognito <> " invited to group #team via your group link")
alice <## ("#team: " <> bobIncognito <> " joined the group"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again"
bob <## ("#team: you joined the group incognito as " <> bobIncognito)
]
-- cath joins incognito
cath ##> ("/c i " <> gLink)
cathIncognito <- getTermLine cath
cath <## "connection request sent incognito!"
alice <## (cathIncognito <> ": accepting request to join group #team...")
_ <- getTermLine cath
concurrentlyN_
[ do
alice <## (cathIncognito <> ": contact is connected")
alice <## (cathIncognito <> " invited to group #team via your group link")
alice <## ("#team: " <> cathIncognito <> " joined the group"),
do
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
cath <## "use /i alice to print out this incognito profile again"
cath <## ("#team: you joined the group incognito as " <> cathIncognito)
cath <## ("#team: member " <> bobIncognito <> " is connected"),
do
bob <## ("#team: alice added " <> cathIncognito <> " to the group (connecting...)")
bob <## ("#team: new member " <> cathIncognito <> " is connected")
]
alice `hasContactProfiles` ["alice", T.pack bobIncognito, T.pack cathIncognito]
bob `hasContactProfiles` ["bob", "alice", T.pack bobIncognito, T.pack cathIncognito]
cath `hasContactProfiles` ["cath", "alice", T.pack bobIncognito, T.pack cathIncognito]
-- bob creates member contact with cath - both share incognito profile
bob ##> ("@#team " <> cathIncognito <> " hi")
bob
<### [ ConsoleString ("member #team " <> cathIncognito <> " does not have direct connection, creating"),
ConsoleString ("contact for member #team " <> cathIncognito <> " is created"),
ConsoleString ("sent invitation to connect directly to member #team " <> cathIncognito),
WithTime ("i @" <> cathIncognito <> " hi")
]
cath
<### [ ConsoleString ("#team " <> bobIncognito <> " is creating direct contact " <> bobIncognito <> " with you"),
WithTime ("i " <> bobIncognito <> "> hi")
]
cath <## (bobIncognito <> ": you can send messages to contact")
_ <- getTermLine bob
_ <- getTermLine cath
concurrentlyN_
[ do
bob <## (cathIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> cathIncognito <> " to print out this incognito profile again"),
do
cath <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> cathIncognito)
cath <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
]
bob `hasContactProfiles` ["bob", "alice", T.pack bobIncognito, T.pack cathIncognito]
cath `hasContactProfiles` ["cath", "alice", T.pack bobIncognito, T.pack cathIncognito]
bob ?#> ("@" <> cathIncognito <> " hi, I'm incognito")
cath ?<# (bobIncognito <> "> hi, I'm incognito")
cath ?#> ("@" <> bobIncognito <> " hey, me too")
bob ?<# (cathIncognito <> "> hey, me too")
-- members still use incognito profile for group
alice #> "#team hello"
concurrentlyN_
[ bob ?<# "#team alice> hello",
cath ?<# "#team alice> hello"
]
bob ?#> "#team hi there"
concurrentlyN_
[ alice <# ("#team " <> bobIncognito <> "> hi there"),
cath ?<# ("#team " <> bobIncognito <> "> hi there")
]
cath ?#> "#team hey"
concurrentlyN_
[ alice <# ("#team " <> cathIncognito <> "> hey"),
bob ?<# ("#team " <> cathIncognito <> "> hey")
]
testMemberContactProfileUpdate :: HasCallStack => TestParams -> IO ()
testMemberContactProfileUpdate =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
bob ##> "/p rob Rob"
bob <## "user profile is changed to rob (Rob) (your 1 contacts are notified)"
alice <## "contact bob changed to rob (Rob)"
alice <## "use @rob <message> to send messages"
cath ##> "/p kate Kate"
cath <## "user profile is changed to kate (Kate) (your 1 contacts are notified)"
alice <## "contact cath changed to kate (Kate)"
alice <## "use @kate <message> to send messages"
alice #> "#team hello"
bob <# "#team alice> hello"
cath <# "#team alice> hello"
alice `hasContactProfiles` ["alice", "rob", "kate"]
bob `hasContactProfiles` ["rob", "alice", "cath"]
cath `hasContactProfiles` ["kate", "alice", "bob"]
bob `send` "@cath hi"
bob
<### [ "member #team cath does not have direct connection, creating",
"contact for member #team cath is created",
"sent invitation to connect directly to member #team cath",
WithTime "@cath hi"
]
cath
<### [ "#team bob is creating direct contact bob with you",
WithTime "bob> hi"
]
cath <## "bob (Bob): you can send messages to contact"
concurrentlyN_
[ do
bob <## "contact cath changed to kate (Kate)"
bob <## "use @kate <message> to send messages"
bob <## "kate (Kate): contact is connected",
do
cath <## "contact bob changed to rob (Rob)"
cath <## "use @rob <message> to send messages"
cath <## "rob (Rob): contact is connected"
]
bob ##> "/contacts"
bob
<### [ "alice (Alice)",
"kate (Kate)"
]
cath ##> "/contacts"
cath
<### [ "alice (Alice)",
"rob (Rob)"
]
alice `hasContactProfiles` ["alice", "rob", "kate"]
bob `hasContactProfiles` ["rob", "alice", "kate"]
cath `hasContactProfiles` ["kate", "alice", "rob"]
bob #> "#team hello too"
alice <# "#team rob> hello too"
cath <# "#team rob> hello too" -- updated profile
cath #> "#team hello there"
alice <# "#team kate> hello there"
bob <# "#team kate> hello there" -- updated profile
testRecreateMemberContactManyGroups :: HasCallStack => TestParams -> IO ()
testRecreateMemberContactManyGroups =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
createGroup2' "team" alice bob False
createGroup2' "club" alice bob False
-- alice can message bob via team and via club
alice ##> "@#team bob 1"
alice <# "@bob 1"
bob <# "alice> 1"
bob ##> "@#team alice 2"
bob <# "@alice 2"
alice <# "bob> 2"
alice ##> "@#club bob 3"
alice <# "@bob 3"
bob <# "alice> 3"
bob ##> "@#club alice 4"
bob <# "@alice 4"
alice <# "bob> 4"
-- alice deletes contact with bob
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
bob ##> "/d alice"
bob <## "alice: contact is deleted"
-- alice creates member contact with bob
alice ##> "@#team bob hi"
alice
<### [ "member #team bob does not have direct connection, creating",
"contact for member #team bob is created",
"sent invitation to connect directly to member #team bob",
WithTime "@bob hi"
]
bob
<### [ "#team alice is creating direct contact alice with you",
WithTime "alice> hi"
]
bob <## "alice (Alice): you can send messages to contact"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
-- alice can message bob via team and via club
alice ##> "@#team bob 1"
alice <# "@bob 1"
bob <# "alice> 1"
bob ##> "@#team alice 2"
bob <# "@alice 2"
alice <# "bob> 2"
alice ##> "@#club bob 3"
alice <# "@bob 3"
bob <# "alice> 3"
bob ##> "@#club alice 4"
bob <# "@alice 4"
alice <# "bob> 4"
testGroupMsgForward :: HasCallStack => TestParams -> IO ()
testGroupMsgForward =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath <# "#team bob> hi there [>>]"
threadDelay 1000000
cath #> "#team hey team"
alice <# "#team cath> hey team"
bob <# "#team cath> hey team [>>]"
alice ##> "/tail #team 2"
alice <# "#team bob> hi there"
alice <# "#team cath> hey team"
bob ##> "/tail #team 2"
bob <# "#team hi there"
bob <# "#team cath> hey team [>>]"
cath ##> "/tail #team 2"
cath <# "#team bob> hi there [>>]"
cath <# "#team hey team"
setupGroupForwarding3 :: String -> TestCC -> TestCC -> TestCC -> IO ()
setupGroupForwarding3 gName alice bob cath = do
createGroup3 gName alice bob cath
threadDelay 1000000 -- delay so intro_status doesn't get overwritten to connected
void $ withCCTransaction bob $ \db ->
DB.execute_ db "UPDATE connections SET conn_status='deleted' WHERE group_member_id = 3"
void $ withCCTransaction cath $ \db ->
DB.execute_ db "UPDATE connections SET conn_status='deleted' WHERE group_member_id = 3"
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'"
testGroupMsgForwardDeduplicate :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardDeduplicate =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000 -- delay so intro_status doesn't get overwritten to connected
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'"
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath
<### [ Predicate ("#team bob> hi there" `isInfixOf`),
StartsWith "duplicate group message, group id: 1"
]
threadDelay 1000000
-- cath sends x.grp.mem.con on deduplication, so alice doesn't forward anymore
cath #> "#team hey team"
alice <# "#team cath> hey team"
bob <# "#team cath> hey team"
alice ##> "/tail #team 2"
alice <# "#team bob> hi there"
alice <# "#team cath> hey team"
bob ##> "/tail #team 2"
bob <# "#team hi there"
bob <# "#team cath> hey team"
cath ##> "/tail #team 2"
cath <#. "#team bob> hi there"
cath <# "#team hey team"
testGroupMsgForwardEdit :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardEdit =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath <# "#team bob> hi there [>>]"
bob ##> "! #team hello there"
bob <# "#team [edited] hello there"
alice <# "#team bob> [edited] hello there"
cath <# "#team bob> [edited] hello there" -- TODO show as forwarded
alice ##> "/tail #team 1"
alice <# "#team bob> hello there"
bob ##> "/tail #team 1"
bob <# "#team hello there"
cath ##> "/tail #team 1"
cath <# "#team bob> hello there [>>]"
testGroupMsgForwardReaction :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardReaction =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath <# "#team bob> hi there [>>]"
cath ##> "+1 #team hi there"
cath <## "added 👍"
alice <# "#team cath> > bob hi there"
alice <## " + 👍"
bob <# "#team cath> > bob hi there"
bob <## " + 👍"
testGroupMsgForwardDeletion :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardDeletion =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
bob #> "#team hi there"
alice <# "#team bob> hi there"
cath <# "#team bob> hi there [>>]"
bob ##> "\\ #team hi there"
bob <## "message marked deleted"
alice <# "#team bob> [marked deleted] hi there"
cath <# "#team bob> [marked deleted] hi there" -- TODO show as forwarded
testGroupMsgForwardFile :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardFile =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
setupGroupForwarding3 "team" alice bob cath
bob #> "/f #team ./tests/fixtures/test.jpg"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (test.jpg) for #team"
concurrentlyN_
[ do
alice <# "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it",
do
cath <# "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it [>>]"
]
cath ##> "/fr 1 ./tests/tmp"
cath <## "saving file 1 from bob to ./tests/tmp/test.jpg"
cath <## "started receiving file 1 (test.jpg) from bob"
cath <## "completed receiving file 1 (test.jpg) from bob"
src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src
testGroupMsgForwardChangeRole :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardChangeRole =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
cath ##> "/mr #team bob member"
cath <## "#team: you changed the role of bob from admin to member"
alice <## "#team: cath changed the role of bob from admin to member"
bob <## "#team: cath changed your role from admin to member" -- TODO show as forwarded
testGroupMsgForwardNewMember :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardNewMember =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
setupGroupForwarding3 "team" alice bob cath
connectUsers cath dan
cath ##> "/a #team dan"
cath <## "invitation to join the group #team sent to dan"
dan <## "#team: cath invites you to join the group as member"
dan <## "use /j team to accept"
dan ##> "/j #team"
dan <## "#team: you joined the group"
concurrentlyN_
[ cath <## "#team: dan joined the group",
do
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected",
-- bob will not connect to dan, as introductions are not forwarded (yet?)
bob <## "#team: cath added dan (Daniel) to the group (connecting...)", -- TODO show as forwarded
dan <## "#team: member alice (Alice) is connected"
]
dan #> "#team hello all"
alice <# "#team dan> hello all"
-- bob <# "#team dan> hello all [>>]"
cath <# "#team dan> hello all"
bob #> "#team hi all"
alice <# "#team bob> hi all"
cath <# "#team bob> hi all [>>]"
-- dan <# "#team bob> hi all [>>]"
bob ##> "/ms team"
bob
<### [ "alice (Alice): owner, host, connected",
"bob (Bob): admin, you, connected",
"cath (Catherine): admin, connected",
"dan (Daniel): member"
]
testGroupMsgForwardLeave :: HasCallStack => TestParams -> IO ()
testGroupMsgForwardLeave =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
bob ##> "/leave #team"
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group"
alice <## "#team: bob left the group"
cath <## "#team: bob left the group"
testGroupHistory :: HasCallStack => TestParams -> IO ()
testGroupHistory =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team hello"
bob <# "#team alice> hello"
threadDelay 1000000
bob #> "#team hey!"
alice <# "#team bob> hey!"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> hello [>>]",
WithTime "#team bob> hey! [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine cath
r `shouldContain` [(0, "hello"), (0, "hey!")]
-- message delivery works after sending history
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testGroupHistoryGroupLink :: HasCallStack => TestParams -> IO ()
testGroupHistoryGroupLink =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team hello"
bob <# "#team alice> hello"
threadDelay 1000000
bob #> "#team hey!"
alice <# "#team bob> hey!"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
alice <## "cath (Catherine): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: joining the group...",
"#team: you joined the group",
WithTime "#team alice> hello [>>]",
WithTime "#team bob> hey! [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine cath
r `shouldContain` [(0, "hello"), (0, "hey!")]
-- message delivery works after sending history
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testGroupHistoryPreferenceOff :: HasCallStack => TestParams -> IO ()
testGroupHistoryPreferenceOff =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team hello"
bob <# "#team alice> hello"
threadDelay 1000000
bob #> "#team hey!"
alice <# "#team bob> hey!"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> hello [>>]",
WithTime "#team bob> hey! [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine cath
r `shouldContain` [(0, "hello"), (0, "hey!")]
alice ##> "/set history #team off"
alice <## "updated group preferences:"
alice <## "Recent history: off"
concurrentlyN_
[ do
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Recent history: off",
do
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Recent history: off"
]
connectUsers alice dan
addMember "team" alice dan GRAdmin
dan ##> "/j team"
concurrentlyN_
[ alice <## "#team: dan joined the group",
do
dan <## "#team: you joined the group"
dan
<### [ "#team: member bob (Bob) is connected",
"#team: member cath (Catherine) is connected"
],
aliceAddedDan bob,
aliceAddedDan cath
]
dan ##> "/_get chat #1 count=100"
r' <- chat <$> getTermLine dan
r' `shouldNotContain` [(0, "hello")]
r' `shouldNotContain` [(0, "hey!")]
-- message delivery works
alice #> "#team 1"
[bob, cath, dan] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath, dan] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob, dan] *<# "#team cath> 3"
dan #> "#team 4"
[alice, bob, cath] *<# "#team dan> 4"
where
aliceAddedDan :: HasCallStack => TestCC -> IO ()
aliceAddedDan cc = do
cc <## "#team: alice added dan (Daniel) to the group (connecting...)"
cc <## "#team: new member dan is connected"
testGroupHistoryHostFile :: HasCallStack => TestParams -> IO ()
testGroupHistoryHostFile =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
createGroup2 "team" alice bob
alice #> "/f #team ./tests/fixtures/test.jpg"
alice <## "use /fc 1 to cancel sending"
alice <## "completed uploading file 1 (test.jpg) for #team"
bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]",
"use /fr 1 [<dir>/ | <path>] to receive it [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/fr 1 ./tests/tmp"
cath
<### [ "saving file 1 from alice to ./tests/tmp/test.jpg",
"started receiving file 1 (test.jpg) from alice"
]
cath <## "completed receiving file 1 (test.jpg) from alice"
src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src
testGroupHistoryMemberFile :: HasCallStack => TestParams -> IO ()
testGroupHistoryMemberFile =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
createGroup2 "team" alice bob
bob #> "/f #team ./tests/fixtures/test.jpg"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (test.jpg) for #team"
alice <# "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]",
"use /fr 1 [<dir>/ | <path>] to receive it [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/fr 1 ./tests/tmp"
cath
<### [ "saving file 1 from bob to ./tests/tmp/test.jpg",
"started receiving file 1 (test.jpg) from bob"
]
cath <## "completed receiving file 1 (test.jpg) from bob"
src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src
testGroupHistoryLargeFile :: HasCallStack => TestParams -> IO ()
testGroupHistoryLargeFile =
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile"]
createGroup2 "team" alice bob
bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"hello\",\"type\":\"file\"}}]"
bob <# "#team hello"
bob <# "/f #team ./tests/tmp/testfile"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (testfile) for #team"
alice <# "#team bob> hello"
alice <# "#team bob> sends file testfile (17.0 MiB / 17825792 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
-- admin receiving file does not prevent the new member from receiving it later
alice ##> "/fr 1 ./tests/tmp"
alice
<### [ "saving file 1 from bob to ./tests/tmp/testfile_1",
"started receiving file 1 (testfile) from bob"
]
alice <## "completed receiving file 1 (testfile) from bob"
src <- B.readFile "./tests/tmp/testfile"
destAlice <- B.readFile "./tests/tmp/testfile_1"
destAlice `shouldBe` src
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team bob> hello [>>]",
WithTime "#team bob> sends file testfile (17.0 MiB / 17825792 bytes) [>>]",
"use /fr 1 [<dir>/ | <path>] to receive it [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/fr 1 ./tests/tmp"
cath
<### [ "saving file 1 from bob to ./tests/tmp/testfile_2",
"started receiving file 1 (testfile) from bob"
]
cath <## "completed receiving file 1 (testfile) from bob"
destCath <- B.readFile "./tests/tmp/testfile_2"
destCath `shouldBe` src
where
cfg = testCfg {xftpDescrPartSize = 200}
testGroupHistoryMultipleFiles :: HasCallStack => TestParams -> IO ()
testGroupHistoryMultipleFiles =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
createGroup2 "team" alice bob
threadDelay 1000000
bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]"
bob <# "#team hi alice"
bob <# "/f #team ./tests/tmp/testfile_bob"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (testfile_bob) for #team"
alice <# "#team bob> hi alice"
alice <# "#team bob> sends file testfile_bob (2.0 MiB / 2097152 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
threadDelay 1000000
alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]"
alice <# "#team hey bob"
alice <# "/f #team ./tests/tmp/testfile_alice"
alice <## "use /fc 2 to cancel sending"
alice <## "completed uploading file 2 (testfile_alice) for #team"
bob <# "#team alice> hey bob"
bob <# "#team alice> sends file testfile_alice (1.0 MiB / 1048576 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team bob> hi alice [>>]",
WithTime "#team bob> sends file testfile_bob (2.0 MiB / 2097152 bytes) [>>]",
"use /fr 1 [<dir>/ | <path>] to receive it [>>]",
WithTime "#team alice> hey bob [>>]",
WithTime "#team alice> sends file testfile_alice (1.0 MiB / 1048576 bytes) [>>]",
"use /fr 2 [<dir>/ | <path>] to receive it [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/fr 1 ./tests/tmp"
cath
<### [ "saving file 1 from bob to ./tests/tmp/testfile_bob_1",
"started receiving file 1 (testfile_bob) from bob"
]
cath <## "completed receiving file 1 (testfile_bob) from bob"
srcBob <- B.readFile "./tests/tmp/testfile_bob"
destBob <- B.readFile "./tests/tmp/testfile_bob_1"
destBob `shouldBe` srcBob
cath ##> "/fr 2 ./tests/tmp"
cath
<### [ "saving file 2 from alice to ./tests/tmp/testfile_alice_1",
"started receiving file 2 (testfile_alice) from alice"
]
cath <## "completed receiving file 2 (testfile_alice) from alice"
srcAlice <- B.readFile "./tests/tmp/testfile_alice"
destAlice <- B.readFile "./tests/tmp/testfile_alice_1"
destAlice `shouldBe` srcAlice
cath ##> "/_get chat #1 count=100"
r <- chatF <$> getTermLine cath
r
`shouldContain` [ ((0, "hi alice"), Just "./tests/tmp/testfile_bob_1"),
((0, "hey bob"), Just "./tests/tmp/testfile_alice_1")
]
testGroupHistoryFileCancel :: HasCallStack => TestParams -> IO ()
testGroupHistoryFileCancel =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
createGroup2 "team" alice bob
bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]"
bob <# "#team hi alice"
bob <# "/f #team ./tests/tmp/testfile_bob"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (testfile_bob) for #team"
alice <# "#team bob> hi alice"
alice <# "#team bob> sends file testfile_bob (2.0 MiB / 2097152 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fc 1"
bob <## "cancelled sending file 1 (testfile_bob) to alice"
alice <## "bob cancelled sending file 1 (testfile_bob)"
threadDelay 1000000
alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]"
alice <# "#team hey bob"
alice <# "/f #team ./tests/tmp/testfile_alice"
alice <## "use /fc 2 to cancel sending"
alice <## "completed uploading file 2 (testfile_alice) for #team"
bob <# "#team alice> hey bob"
bob <# "#team alice> sends file testfile_alice (1.0 MiB / 1048576 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice ##> "/fc 2"
alice <## "cancelled sending file 2 (testfile_alice) to bob"
bob <## "alice cancelled sending file 2 (testfile_alice)"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team bob> hi alice [>>]",
WithTime "#team alice> hey bob [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
testGroupHistoryFileCancelNoText :: HasCallStack => TestParams -> IO ()
testGroupHistoryFileCancelNoText =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
createGroup2 "team" alice bob
alice #> "#team hello"
bob <# "#team alice> hello"
-- bob file
bob #> "/f #team ./tests/tmp/testfile_bob"
bob <## "use /fc 1 to cancel sending"
bob <## "completed uploading file 1 (testfile_bob) for #team"
alice <# "#team bob> sends file testfile_bob (2.0 MiB / 2097152 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob ##> "/fc 1"
bob <## "cancelled sending file 1 (testfile_bob) to alice"
alice <## "bob cancelled sending file 1 (testfile_bob)"
-- alice file
alice #> "/f #team ./tests/tmp/testfile_alice"
alice <## "use /fc 2 to cancel sending"
alice <## "completed uploading file 2 (testfile_alice) for #team"
bob <# "#team alice> sends file testfile_alice (1.0 MiB / 1048576 bytes)"
bob <## "use /fr 2 [<dir>/ | <path>] to receive it"
alice ##> "/fc 2"
alice <## "cancelled sending file 2 (testfile_alice) to bob"
bob <## "alice cancelled sending file 2 (testfile_alice)"
-- other messages are sent
bob #> "#team hey!"
alice <# "#team bob> hey!"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> hello [>>]",
WithTime "#team bob> hey! [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
testGroupHistoryQuotes :: HasCallStack => TestParams -> IO ()
testGroupHistoryQuotes =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team ALICE"
bob <# "#team alice> ALICE"
threadDelay 1000000
bob #> "#team BOB"
alice <# "#team bob> BOB"
threadDelay 1000000
alice `send` "> #team @alice (ALICE) 1"
alice <# "#team > alice ALICE"
alice <## " 1"
bob <# "#team alice> > alice ALICE"
bob <## " 1"
threadDelay 1000000
alice `send` "> #team @bob (BOB) 2"
alice <# "#team > bob BOB"
alice <## " 2"
bob <# "#team alice!> > bob BOB"
bob <## " 2"
threadDelay 1000000
bob `send` "> #team @alice (ALICE) 3"
bob <# "#team > alice ALICE"
bob <## " 3"
alice <# "#team bob!> > alice ALICE"
alice <## " 3"
threadDelay 1000000
bob `send` "> #team @bob (BOB) 4"
bob <# "#team > bob BOB"
bob <## " 4"
alice <# "#team bob> > bob BOB"
alice <## " 4"
alice
#$> ( "/_get chat #1 count=6",
chat',
[ ((1, "ALICE"), Nothing),
((0, "BOB"), Nothing),
((1, "1"), Just (1, "ALICE")),
((1, "2"), Just (0, "BOB")),
((0, "3"), Just (1, "ALICE")),
((0, "4"), Just (0, "BOB"))
]
)
bob
#$> ( "/_get chat #1 count=6",
chat',
[ ((0, "ALICE"), Nothing),
((1, "BOB"), Nothing),
((0, "1"), Just (0, "ALICE")),
((0, "2"), Just (1, "BOB")),
((1, "3"), Just (0, "ALICE")),
((1, "4"), Just (1, "BOB"))
]
)
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> ALICE [>>]",
WithTime "#team bob> BOB [>>]",
WithTime "#team alice> > alice ALICE [>>]",
" 1 [>>]",
WithTime "#team alice> > bob BOB [>>]",
" 2 [>>]",
WithTime "#team bob> > alice ALICE [>>]",
" 3 [>>]",
WithTime "#team bob> > bob BOB [>>]",
" 4 [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat' <$> getTermLine cath
r
`shouldContain` [ ((0, "ALICE"), Nothing),
((0, "BOB"), Nothing),
((0, "1"), Just (0, "ALICE")),
((0, "2"), Just (0, "BOB")),
((0, "3"), Just (0, "ALICE")),
((0, "4"), Just (0, "BOB"))
]
testGroupHistoryDeletedMessage :: HasCallStack => TestParams -> IO ()
testGroupHistoryDeletedMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
disableFullDeletion2 "team" alice bob
alice #> "#team hello"
bob <# "#team alice> hello"
threadDelay 1000000
bob #> "#team hey!"
alice <# "#team bob> hey!"
bobMsgId <- lastItemId bob
bob #$> ("/_delete item #1 " <> bobMsgId <> " broadcast", id, "message marked deleted")
alice <# "#team bob> [marked deleted] hey!"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> hello [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine cath
r `shouldContain` [(0, "hello")]
r `shouldNotContain` [(0, "hey!")]
testGroupHistoryDisappearingMessage :: HasCallStack => TestParams -> IO ()
testGroupHistoryDisappearingMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
threadDelay 1000000
alice #> "#team 1"
bob <# "#team alice> 1"
threadDelay 1000000
-- 3 seconds so that messages 2 and 3 are not deleted for alice before sending history to cath
alice ##> "/set disappear #team on 4"
alice <## "updated group preferences:"
alice <## "Disappearing messages: on (4 sec)"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Disappearing messages: on (4 sec)"
bob #> "#team 2"
alice <# "#team bob> 2"
threadDelay 1000000
alice #> "#team 3"
bob <# "#team alice> 3"
threadDelay 1000000
alice ##> "/set disappear #team off"
alice <## "updated group preferences:"
alice <## "Disappearing messages: off"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Disappearing messages: off"
bob #> "#team 4"
alice <# "#team bob> 4"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> 1 [>>]",
WithTime "#team bob> 2 [>>]",
WithTime "#team alice> 3 [>>]",
WithTime "#team bob> 4 [>>]",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r1 <- chat <$> getTermLine cath
r1 `shouldContain` [(0, "1"), (0, "2"), (0, "3"), (0, "4")]
concurrentlyN_
[ alice
<### [ "timed message deleted: 2",
"timed message deleted: 3"
],
bob
<### [ "timed message deleted: 2",
"timed message deleted: 3"
],
cath
<### [ "timed message deleted: 2",
"timed message deleted: 3"
]
]
cath ##> "/_get chat #1 count=100"
r2 <- chat <$> getTermLine cath
r2 `shouldContain` [(0, "1"), (0, "4")]
r2 `shouldNotContain` [(0, "2")]
r2 `shouldNotContain` [(0, "3")]
testGroupHistoryWelcomeMessage :: HasCallStack => TestParams -> IO ()
testGroupHistoryWelcomeMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice bob
alice ##> "/set welcome #team welcome to team"
alice <## "description changed to:"
alice <## "welcome to team"
bob <## "alice updated group #team:"
bob <## "description changed to:"
bob <## "welcome to team"
threadDelay 1000000
alice #> "#team hello"
bob <# "#team alice> hello"
threadDelay 1000000
bob #> "#team hey!"
alice <# "#team bob> hey!"
connectUsers alice cath
addMember "team" alice cath GRAdmin
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
cath
<### [ "#team: you joined the group",
WithTime "#team alice> hello [>>]",
WithTime "#team bob> hey! [>>]",
WithTime "#team alice> welcome to team",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
cath ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine cath
-- sometimes there are "connected" and feature items in between,
-- so we filter them out; `shouldContain` then checks order is correct
let expected = [(0, "hello"), (0, "hey!"), (0, "welcome to team")]
r' = filter (`elem` expected) r
r' `shouldContain` expected
-- message delivery works after sending history
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testGroupHistoryUnknownMember :: HasCallStack => TestParams -> IO ()
testGroupHistoryUnknownMember =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice #> "#team hi from alice"
[bob, cath] *<# "#team alice> hi from alice"
threadDelay 1000000
bob #> "#team hi from bob"
[alice, cath] *<# "#team bob> hi from bob"
threadDelay 1000000
cath #> "#team hi from cath"
[alice, bob] *<# "#team cath> hi from cath"
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group",
cath <## "#team: bob left the group"
]
connectUsers alice dan
addMember "team" alice dan GRAdmin
dan ##> "/j team"
concurrentlyN_
[ alice <## "#team: dan joined the group",
dan
<### [ "#team: you joined the group",
WithTime "#team alice> hi from alice [>>]",
StartsWith "#team: alice forwarded a message from an unknown member, creating unknown member record",
EndsWith "hi from bob [>>]",
WithTime "#team cath> hi from cath [>>]",
"#team: member cath (Catherine) is connected"
],
do
cath <## "#team: alice added dan (Daniel) to the group (connecting...)"
cath <## "#team: new member dan is connected"
]
dan ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine dan
r `shouldContain` [(0, "hi from alice"), (0, "hi from bob"), (0, "hi from cath")]
dan ##> "/ms team"
dan
<### [ "dan (Daniel): admin, you, connected",
"alice (Alice): owner, host, connected",
"cath (Catherine): admin, connected",
EndsWith "author, status unknown"
]
-- message delivery works after sending history
alice #> "#team 1"
[cath, dan] *<# "#team alice> 1"
cath #> "#team 2"
[alice, dan] *<# "#team cath> 2"
dan #> "#team 3"
[alice, cath] *<# "#team dan> 3"
testMembershipProfileUpdateNextGroupMessage :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateNextGroupMessage =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
-- create group 1
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLinkTeam <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLinkTeam)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/create link #club"
gLinkClub <- getGroupLink alice "club" GRMember True
cath ##> ("/c " <> gLinkClub)
cath <## "connection request sent!"
alice <## "cath (Catherine): accepting request to join group #club..."
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: joining the group..."
cath <## "#club: you joined the group"
]
-- alice has no contacts
alice ##> "/contacts"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice #> "#club hello club"
cath <# "#club alice> hello club"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 0 contacts are notified)"
-- update profile in group 1
bob ##> "/ms team"
bob
<### [ "bob (Bob): member, you, connected",
"alice (Alice): owner, host, connected"
]
alice #> "#team team 1"
bob <# "#team alisa> team 1"
cath <// 50000
bob ##> "/ms team"
bob
<### [ "bob (Bob): member, you, connected",
"alisa: owner, host, connected"
]
alice #> "#team team 2"
bob <# "#team alisa> team 2"
bob ##> "/_get chat #1 count=100"
rb <- chat <$> getTermLine bob
rb `shouldContain` [(0, "updated profile")]
-- update profile in group 2
cath ##> "/ms club"
cath
<### [ "cath (Catherine): member, you, connected",
"alice (Alice): owner, host, connected"
]
alice #> "#club club 1"
cath <# "#club alisa> club 1"
cath ##> "/ms club"
cath
<### [ "cath (Catherine): member, you, connected",
"alisa: owner, host, connected"
]
alice #> "#club club 2"
cath <# "#club alisa> club 2"
cath ##> "/_get chat #1 count=100"
rc <- chat <$> getTermLine cath
rc `shouldContain` [(0, "updated profile")]
testMembershipProfileUpdateSameMember :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateSameMember =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
createGroup2' "club" alice bob False
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 0 contacts are notified)"
bob `hasContactProfiles` ["alice", "bob"]
alice #> "#team team 1"
bob <## "contact alice changed to alisa"
bob <## "use @alisa <message> to send messages"
bob <# "#team alisa> team 1"
-- since members were related to the same contact, both member records are updated
bob `hasContactProfiles` ["alisa", "bob"]
checkMembers bob
checkItems bob
-- profile update is not processed in second group, since it hasn't changed
alice #> "#club club 1"
bob <# "#club alisa> club 1"
bob `hasContactProfiles` ["alisa", "bob"]
checkMembers bob
checkItems bob
where
checkMembers bob = do
bob ##> "/ms team"
bob
<### [ "bob (Bob): admin, you, connected",
"alisa: owner, host, connected"
]
bob ##> "/ms club"
bob
<### [ "bob (Bob): admin, you, connected",
"alisa: owner, host, connected"
]
checkItems bob = do
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldNotContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rTeam <- chat <$> getTermLine bob
rTeam `shouldContain` [(0, "updated profile")]
bob ##> "/_get chat #2 count=100"
rClub <- chat <$> getTermLine bob
rClub `shouldNotContain` [(0, "updated profile")]
testMembershipProfileUpdateContactActive :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateContactActive =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/contacts"
alice <## "bob (Bob)"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 1 contacts are notified)"
bob <## "contact alice changed to alisa"
bob <## "use @alisa <message> to send messages"
bob `hasContactProfiles` ["alisa", "bob"]
alice #> "#team team 1"
bob <# "#team alisa> team 1"
bob `hasContactProfiles` ["alisa", "bob"]
checkItems bob
alice ##> "/ad"
cLink <- getContactLink alice True
alice ##> "/pa on"
alice <## "new contact address set"
bob <## "alisa set new contact address, use /info alisa to view"
bob `hasContactProfiles` ["alisa", "bob"]
checkAliceProfileLink bob "alisa" cLink
-- profile update does not remove contact address from profile
alice ##> "/p 'Alice Smith'"
alice <## "user profile is changed to 'Alice Smith' (your 1 contacts are notified)"
bob <## "contact alisa changed to 'Alice Smith'"
bob <## "use @'Alice Smith' <message> to send messages"
bob `hasContactProfiles` ["Alice Smith", "bob"]
checkAliceProfileLink bob "'Alice Smith'" cLink
-- receiving group message does not remove contact address from profile
alice #> "#team team 2"
bob <# "#team 'Alice Smith'> team 2"
bob `hasContactProfiles` ["Alice Smith", "bob"]
checkAliceProfileLink bob "'Alice Smith'" cLink
checkItems bob
where
checkItems bob = do
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rGrp <- chat <$> getTermLine bob
rGrp `shouldNotContain` [(0, "updated profile")]
checkAliceProfileLink bob name cLink = do
bob ##> ("/info #team " <> name)
bob <## "group ID: 1"
bob <## "member ID: 1"
bob <##. "receiving messages via"
bob <##. "sending messages via"
bob <## ("contact address: " <> cLink)
bob <## "connection not verified, use /code command to see security code"
bob <## currentChatVRangeInfo
testMembershipProfileUpdateContactDeleted :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateContactDeleted =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/contacts"
alice <## "bob (Bob)"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 0 contacts are notified)"
bob `hasContactProfiles` ["alice", "bob"]
alice #> "#team team 1"
bob <## "contact alice changed to alisa"
bob <## "use @alisa <message> to send messages"
bob <# "#team alisa> team 1"
bob `hasContactProfiles` ["alisa", "bob"]
checkItems bob
-- adding contact address to profile does not share it with member
alice ##> "/ad"
_ <- getContactLink alice True
alice ##> "/pa on"
alice <## "new contact address set"
bob `hasContactProfiles` ["alisa", "bob"]
checkAliceNoProfileLink bob "alisa"
alice #> "#team team 2"
bob <# "#team alisa> team 2"
bob `hasContactProfiles` ["alisa", "bob"]
checkAliceNoProfileLink bob "alisa"
-- profile update does not add contact address to member profile
alice ##> "/p 'Alice Smith'"
alice <## "user profile is changed to 'Alice Smith' (your 0 contacts are notified)"
bob `hasContactProfiles` ["alisa", "bob"]
checkAliceNoProfileLink bob "alisa"
alice #> "#team team 3"
bob <## "contact alisa changed to 'Alice Smith'"
bob <## "use @'Alice Smith' <message> to send messages"
bob <# "#team 'Alice Smith'> team 3"
bob `hasContactProfiles` ["Alice Smith", "bob"]
checkAliceNoProfileLink bob "'Alice Smith'"
checkItems bob
where
checkItems bob = do
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldNotContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rGrp <- chat <$> getTermLine bob
rGrp `shouldContain` [(0, "updated profile")]
checkAliceNoProfileLink bob name = do
bob ##> ("/info #team " <> name)
bob <## "group ID: 1"
bob <## "member ID: 1"
bob <##. "receiving messages via"
bob <##. "sending messages via"
bob <## "connection not verified, use /code command to see security code"
bob <## currentChatVRangeInfo
testMembershipProfileUpdateContactDisabled :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateContactDisabled =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/contacts"
alice <## "bob (Bob)"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice ##> "/_delete @2 notify=off"
alice <## "bob: contact is deleted"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 0 contacts are notified)"
bob `hasContactProfiles` ["alice", "bob"]
-- bob expects update from contact, so he doesn't update profile
alice #> "#team team 1"
bob <# "#team alice> team 1"
bob `hasContactProfiles` ["alice", "bob"]
-- bob sends any message to alice, increases auth err counter
bob `send` "/feed hi all"
bob <##. "/feed (1)"
bob <## "[alice, 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"
-- on next profile update from alice member, bob considers contact disabled for purposes of profile update
alice #> "#team team 2"
bob <# "#team alice> team 2"
bob `hasContactProfiles` ["alice", "bob"]
alice ##> "/p 'Alice Smith'"
alice <## "user profile is changed to 'Alice Smith' (your 0 contacts are notified)"
alice #> "#team team 3"
bob <## "contact alice changed to 'Alice Smith'"
bob <## "use @'Alice Smith' <message> to send messages"
bob <# "#team 'Alice Smith'> team 3"
bob `hasContactProfiles` ["Alice Smith", "bob"]
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldNotContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rGrp <- chat <$> getTermLine bob
rGrp `shouldContain` [(0, "updated profile")]
testMembershipProfileUpdateNoChangeIgnored :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateNoChangeIgnored =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/contacts"
alice <## "bob (Bob)"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "/p alisa"
alice <## "user profile is changed to alisa (your 0 contacts are notified)"
bob `hasContactProfiles` ["alice", "bob"]
alice ##> "/p alice Alice"
alice <## "user profile is changed to alice (Alice) (your 0 contacts are notified)"
bob `hasContactProfiles` ["alice", "bob"]
alice #> "#team team 1"
bob <# "#team alice> team 1"
bob `hasContactProfiles` ["alice", "bob"]
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldNotContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rGrp <- chat <$> getTermLine bob
rGrp `shouldNotContain` [(0, "updated profile")]
testMembershipProfileUpdateContactLinkIgnored :: HasCallStack => TestParams -> IO ()
testMembershipProfileUpdateContactLinkIgnored =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice ##> "/contacts"
alice <## "bob (Bob)"
alice #> "#team hello team"
bob <# "#team alice> hello team"
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## "alice (Alice) deleted contact with you"
alice ##> "/ad"
_ <- getContactLink alice True
alice ##> "/pa on"
alice <## "new contact address set"
bob `hasContactProfiles` ["alice", "bob"]
alice #> "#team team 1"
bob <# "#team alice> team 1"
bob ##> "/_get chat @2 count=100"
rCt <- chat <$> getTermLine bob
rCt `shouldNotContain` [(0, "updated profile")]
bob ##> "/_get chat #1 count=100"
rGrp <- chat <$> getTermLine bob
rGrp `shouldNotContain` [(0, "updated profile")]
bob ##> "/info #team alice"
bob <## "group ID: 1"
bob <## "member ID: 1"
bob <##. "receiving messages via"
bob <##. "sending messages via"
bob <## "connection not verified, use /code command to see security code"
bob <## currentChatVRangeInfo
testBlockForAllMarkedBlocked :: HasCallStack => TestParams -> IO ()
testBlockForAllMarkedBlocked =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
threadDelay 1000000
bob #> "#team 1"
[alice, cath] *<# "#team bob> 1"
threadDelay 1000000
alice ##> "/block for all #team bob"
alice <## "#team: you blocked bob"
cath <## "#team: alice blocked bob"
bob <// 50000
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): admin, invited, connected, blocked by admin",
"cath (Catherine): admin, invited, connected"
]
cath ##> "/ms team"
cath
<### [ "cath (Catherine): admin, you, connected",
"alice (Alice): owner, host, connected",
"bob (Bob): admin, connected, blocked by admin"
]
bob ##> "/ms team"
bob
<### [ "bob (Bob): admin, you, connected",
"alice (Alice): owner, host, connected",
"cath (Catherine): admin, connected"
]
threadDelay 1000000
bob #> "#team 2"
alice <# "#team bob> 2 [blocked by admin] <muted>"
cath <# "#team bob> 2 [blocked by admin] <muted>"
threadDelay 1000000
bob #> "#team 3"
alice <# "#team bob> 3 [blocked by admin] <muted>"
cath <# "#team bob> 3 [blocked by admin] <muted>"
threadDelay 1000000
alice ##> "/unblock for all #team bob"
alice <## "#team: you unblocked bob"
cath <## "#team: alice unblocked bob"
bob <// 50000
threadDelay 1000000
bob #> "#team 4"
[alice, cath] *<# "#team bob> 4"
alice
#$> ( "/_get chat #1 count=6",
chat,
[ (0, "1"),
(1, "blocked bob (Bob)"),
(0, "2 [blocked by admin]"),
(0, "3 [blocked by admin]"),
(1, "unblocked bob (Bob)"),
(0, "4")
]
)
cath
#$> ( "/_get chat #1 count=6",
chat,
[ (0, "1"),
(0, "blocked bob (Bob)"),
(0, "2 [blocked by admin]"),
(0, "3 [blocked by admin]"),
(0, "unblocked bob (Bob)"),
(0, "4")
]
)
bob #$> ("/_get chat #1 count=4", chat, [(1, "1"), (1, "2"), (1, "3"), (1, "4")])
testBlockForAllFullDelete :: HasCallStack => TestParams -> IO ()
testBlockForAllFullDelete =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
alice ##> "/set delete #team on"
alice <## "updated group preferences:"
alice <## "Full deletion: on"
concurrentlyN_
[ do
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Full deletion: on",
do
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Full deletion: on"
]
threadDelay 1000000
bob #> "#team 1"
[alice, cath] *<# "#team bob> 1"
threadDelay 1000000
alice ##> "/block for all #team bob"
alice <## "#team: you blocked bob"
cath <## "#team: alice blocked bob"
bob <// 50000
threadDelay 1000000
bob #> "#team 2"
alice <# "#team bob> blocked [blocked by admin] <muted>"
cath <# "#team bob> blocked [blocked by admin] <muted>"
threadDelay 1000000
bob #> "#team 3"
alice <# "#team bob> blocked [blocked by admin] <muted>"
cath <# "#team bob> blocked [blocked by admin] <muted>"
threadDelay 1000000
alice ##> "/unblock for all #team bob"
alice <## "#team: you unblocked bob"
cath <## "#team: alice unblocked bob"
bob <// 50000
threadDelay 1000000
bob #> "#team 4"
[alice, cath] *<# "#team bob> 4"
alice
#$> ( "/_get chat #1 count=6",
chat,
[ (0, "1"),
(1, "blocked bob (Bob)"),
(0, "blocked [blocked by admin]"),
(0, "blocked [blocked by admin]"),
(1, "unblocked bob (Bob)"),
(0, "4")
]
)
cath
#$> ( "/_get chat #1 count=6",
chat,
[ (0, "1"),
(0, "blocked bob (Bob)"),
(0, "blocked [blocked by admin]"),
(0, "blocked [blocked by admin]"),
(0, "unblocked bob (Bob)"),
(0, "4")
]
)
bob #$> ("/_get chat #1 count=4", chat, [(1, "1"), (1, "2"), (1, "3"), (1, "4")])
testBlockForAllAnotherAdminUnblocks :: HasCallStack => TestParams -> IO ()
testBlockForAllAnotherAdminUnblocks =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
bob #> "#team 1"
[alice, cath] *<# "#team bob> 1"
alice ##> "/block for all #team bob"
alice <## "#team: you blocked bob"
cath <## "#team: alice blocked bob"
bob <// 50000
bob #> "#team 2"
alice <# "#team bob> 2 [blocked by admin] <muted>"
cath <# "#team bob> 2 [blocked by admin] <muted>"
cath ##> "/unblock for all #team bob"
cath <## "#team: you unblocked bob"
alice <## "#team: cath unblocked bob"
bob <// 50000
bob #> "#team 3"
[alice, cath] *<# "#team bob> 3"
bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")])
testBlockForAllBeforeJoining :: HasCallStack => TestParams -> IO ()
testBlockForAllBeforeJoining =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
bob #> "#team 1"
[alice, cath] *<# "#team bob> 1"
alice ##> "/block for all #team bob"
alice <## "#team: you blocked bob"
cath <## "#team: alice blocked bob"
bob <// 50000
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2 [blocked by admin] <muted>"
connectUsers alice dan
addMember "team" alice dan GRAdmin
dan ##> "/j team"
concurrentlyN_
[ alice <## "#team: dan joined the group",
do
dan <## "#team: you joined the group"
dan
<### [ "#team: member bob (Bob) is connected",
"#team: member cath (Catherine) is connected"
],
aliceAddedDan bob,
aliceAddedDan cath
]
threadDelay 1000000
bob #> "#team 3"
[alice, cath, dan] *<# "#team bob> 3 [blocked by admin] <muted>"
threadDelay 1000000
bob #> "#team 4"
[alice, cath, dan] *<# "#team bob> 4 [blocked by admin] <muted>"
threadDelay 1000000
alice ##> "/unblock for all #team bob"
alice <## "#team: you unblocked bob"
cath <## "#team: alice unblocked bob"
dan <## "#team: alice unblocked bob"
bob <// 50000
threadDelay 1000000
bob #> "#team 5"
[alice, cath, dan] *<# "#team bob> 5"
dan ##> "/_get chat #1 count=100"
r <- chat <$> getTermLine dan
r `shouldContain` [(0, "3 [blocked by admin]"), (0, "4 [blocked by admin]"), (0, "unblocked bob (Bob)"), (0, "5")]
r `shouldNotContain` [(0, "1")]
r `shouldNotContain` [(0, "1 [blocked by admin]")]
r `shouldNotContain` [(0, "2")]
r `shouldNotContain` [(0, "2 [blocked by admin]")]
where
aliceAddedDan :: HasCallStack => TestCC -> IO ()
aliceAddedDan cc = do
cc <## "#team: alice added dan (Daniel) to the group (connecting...)"
cc <## "#team: new member dan is connected"
testBlockForAllCantRepeat :: HasCallStack => TestParams -> IO ()
testBlockForAllCantRepeat =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
disableFullDeletion3 "team" alice bob cath
alice ##> "/unblock for all #team bob"
alice <## "bad chat command: already unblocked"
cath ##> "/unblock for all #team bob"
cath <## "bad chat command: already unblocked"
bob #> "#team 1"
[alice, cath] *<# "#team bob> 1"
alice ##> "/block for all #team bob"
alice <## "#team: you blocked bob"
cath <## "#team: alice blocked bob"
bob <// 50000
alice ##> "/block for all #team bob"
alice <## "bad chat command: already blocked"
cath ##> "/block for all #team bob"
cath <## "bad chat command: already blocked"
bob #> "#team 2"
alice <# "#team bob> 2 [blocked by admin] <muted>"
cath <# "#team bob> 2 [blocked by admin] <muted>"
cath ##> "/unblock for all #team bob"
cath <## "#team: you unblocked bob"
alice <## "#team: cath unblocked bob"
bob <// 50000
alice ##> "/unblock for all #team bob"
alice <## "bad chat command: already unblocked"
cath ##> "/unblock for all #team bob"
cath <## "bad chat command: already unblocked"
bob #> "#team 3"
[alice, cath] *<# "#team bob> 3"
bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")])
testGroupMemberInactive :: HasCallStack => TestParams -> IO ()
testGroupMemberInactive ps = do
withSmpServer' serverCfg' $ do
withNewTestChatCfgOpts ps cfg' opts' "alice" aliceProfile $ \alice -> do
withNewTestChatCfgOpts ps cfg' opts' "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
-- bob is offline
alice #> "#team 1"
alice #> "#team 2"
alice #> "#team 3"
alice <## "[#team bob] connection is marked as inactive"
-- 4 and 5 will be sent to bob as pending messages
alice #> "#team 4"
alice #> "#team 5"
pgmCount <- withCCTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]]
pgmCount `shouldBe` [[2]]
threadDelay 1500000
withTestChatCfgOpts ps cfg' opts' "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <# "#team alice> 1"
bob <# "#team alice> 2"
bob <#. "#team alice> skipped message ID"
alice <## "[#team bob] inactive connection is marked as active"
bob <# "#team alice> 4"
bob <# "#team alice> 5"
pgmCount' <- withCCTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]]
pgmCount' `shouldBe` [[0]]
-- delivery works
alice #> "#team hi"
bob <# "#team alice> hi"
bob #> "#team hey"
alice <# "#team bob> hey"
where
serverCfg' =
smpServerCfg
{ transports = [("7003", transport @TLS, False)],
msgQueueQuota = 2
}
fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} -- same as in agent tests
cfg' =
testCfg
{ agentConfig =
testAgentCfg
{ quotaExceededTimeout = 1,
messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
}
}
opts' =
testOpts
{ coreOptions =
testCoreOpts
{ smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]
}
}
testGroupMemberReports :: HasCallStack => TestParams -> IO ()
testGroupMemberReports =
testChat4 aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
createGroup3 "jokes" alice bob cath
disableFullDeletion3 "jokes" alice bob cath
alice ##> "/mr jokes bob moderator"
concurrentlyN_
[ alice <## "#jokes: you changed the role of bob from admin to moderator",
bob <## "#jokes: alice changed your role from admin to moderator",
cath <## "#jokes: alice changed the role of bob from admin to moderator"
]
alice ##> "/mr jokes cath member"
concurrentlyN_
[ alice <## "#jokes: you changed the role of cath from admin to member",
bob <## "#jokes: alice changed the role of cath from admin to member",
cath <## "#jokes: alice changed your role from admin to member"
]
alice ##> "/create link #jokes"
gLink <- getGroupLink alice "jokes" GRMember True
dan ##> ("/c " <> gLink)
dan <## "connection request sent!"
concurrentlyN_
[ do
alice <## "dan (Daniel): accepting request to join group #jokes..."
alice <## "#jokes: dan joined the group",
do
dan <## "#jokes: joining the group..."
dan <## "#jokes: you joined the group"
dan <###
[ "#jokes: member bob (Bob) is connected",
"#jokes: member cath (Catherine) is connected"
],
do
bob <## "#jokes: alice added dan (Daniel) to the group (connecting...)"
bob <## "#jokes: new member dan is connected",
do
cath <## "#jokes: alice added dan (Daniel) to the group (connecting...)"
cath <## "#jokes: new member dan is connected"
]
cath #> "#jokes inappropriate joke"
concurrentlyN_
[ alice <# "#jokes cath> inappropriate joke",
bob <# "#jokes cath> inappropriate joke",
dan <# "#jokes cath> inappropriate joke"
]
dan ##> "/report #jokes content inappropriate joke"
dan <# "#jokes > cath inappropriate joke"
dan <## " report content"
concurrentlyN_
[ do
alice <# "#jokes dan> > cath inappropriate joke"
alice <## " report content",
do
bob <# "#jokes dan> > cath inappropriate joke"
bob <## " report content",
(cath </)
]
alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")])
bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")])
dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")])
alice ##> "\\\\ #jokes cath inappropriate joke"
concurrentlyN_
[ do
alice <## "#jokes: 1 messages deleted by member alice"
alice <## "message marked deleted by you",
do
bob <# "#jokes cath> [marked deleted by alice] inappropriate joke"
bob <## "#jokes: 1 messages deleted by member alice",
cath <# "#jokes cath> [marked deleted by alice] inappropriate joke",
do
dan <# "#jokes cath> [marked deleted by alice] inappropriate joke"
dan <## "#jokes: 1 messages deleted by member alice"
]
alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")])
bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by alice]")])
dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content [marked deleted by alice]")])
testMemberMention :: HasCallStack => TestParams -> IO ()
testMemberMention =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice #> "#team hello!"
concurrentlyN_
[ bob <# "#team alice> hello!",
cath <# "#team alice> hello!"
]
bob #> "#team hello @alice"
concurrentlyN_
[ alice <# "#team bob!> hello @alice",
cath <# "#team bob> hello @alice"
]
alice #> "#team hello @bob @bob @cath"
concurrentlyN_
[ bob <# "#team alice!> hello @bob @bob @cath",
cath <# "#team alice!> hello @bob @bob @cath"
]
cath #> "#team hello @Alice" -- not a mention
concurrentlyN_
[ alice <# "#team cath> hello @Alice",
bob <# "#team cath> hello @Alice"
]
testUniqueMsgMentions :: SpecWith TestParams
testUniqueMsgMentions = do
it "1 correct mention" $ \_ ->
uniqueMsgMentions 2 (mm [("alice", "abcd")]) ["alice"]
`shouldBe` (mm [("alice", "abcd")])
it "2 correct mentions" $ \_ ->
uniqueMsgMentions 2 (mm [("alice", "abcd"), ("bob", "efgh")]) ["alice", "bob"]
`shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")])
it "2 correct mentions with repetition" $ \_ ->
uniqueMsgMentions 2 (mm [("alice", "abcd"), ("bob", "efgh")]) ["alice", "alice", "alice", "bob", "bob", "bob"]
`shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")])
it "too many mentions - drop extras" $ \_ ->
uniqueMsgMentions 3 (mm [("a", "abcd"), ("b", "efgh"), ("c", "1234"), ("d", "5678")]) ["a", "a", "a", "b", "b", "c", "d"]
`shouldBe` (mm [("a", "abcd"), ("b", "efgh"), ("c", "1234")])
it "repeated-with-different name - drop extras" $ \_ ->
uniqueMsgMentions 2 (mm [("alice", "abcd"), ("alice2", "abcd"), ("bob", "efgh"), ("bob2", "efgh")]) ["alice", "alice2", "bob", "bob2"]
`shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")])
where
mm = M.fromList . map (second $ MemberMention . MemberId)