{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} module Bots.DirectoryTests where import ChatClient import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) import Control.Monad (forM_, when) import qualified Data.Aeson as J import qualified Data.Text as T import Directory.Captcha import Directory.Listing import Directory.Options import Directory.Service import Directory.Store import System.Directory (emptyPermissions, setOwnerExecutable, setOwnerReadable, setOwnerWritable, setPermissions) import System.IO (hClose) import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (ChatConfig (..)) import qualified Simplex.Chat.Markdown as MD import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB import Simplex.Chat.Types (ChatPeerType (..), Profile (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import System.FilePath (()) import Test.Hspec hiding (it) directoryServiceTests :: SpecWith TestParams directoryServiceTests = do it "should register group" testDirectoryService it "should suspend and resume group, send message to owner" testSuspendResume it "should delete group registration" testDeleteGroup it "admin should delete group registration" testDeleteGroupAdmin it "should change initial member role" testSetRole it "should join found group via link" testJoinGroup it "should support group names with spaces" testGroupNameWithSpaces it "should return more groups in search, all and recent groups" testSearchGroups it "should invite to owners' group if specified" testInviteToOwnersGroup describe "de-listing the group" $ do it "should de-list if owner leaves the group" testDelistedOwnerLeaves it "should de-list if owner is removed from the group" testDelistedOwnerRemoved it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved it "should de-list if service is removed from the group" testDelistedServiceRemoved it "should de-list if group is deleted" testDelistedGroupDeleted it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles describe "should require re-approval if profile is changed by" $ do it "the registration owner" testRegOwnerChangedProfile it "another owner" testAnotherOwnerChangedProfile it "another owner not connected to directory" testNotConnectedOwnerChangedProfile describe "should require profile update if group link is removed by " $ do it "the registration owner" testRegOwnerRemovedLink it "another owner" testAnotherOwnerRemovedLink it "another owner not connected to directory" testNotConnectedOwnerRemovedLink describe "duplicate groups (same display name and full name)" $ do it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval describe "list and promote groups" $ do it "should list and promote user's groups" $ testListUserGroups True describe "member admission" $ do it "should ask member to pass captcha screen" testCapthaScreening it "should send voice captcha on /audio command" testVoiceCaptchaScreening it "should retry with voice captcha after switching to audio mode" testVoiceCaptchaRetry it "should send voice captcha when voice disabled but client supports v17" testVoiceCaptchaVoiceDisabled it "should show unavailable message for old client in voice-disabled group" testVoiceCaptchaOldClient it "should reject member after too many captcha attempts" testCaptchaTooManyAttempts it "should respond to unknown command during captcha" testCaptchaUnknownCommand describe "store log" $ do it "should restore directory service state" testRestoreDirectory describe "captcha" $ do it "should accept some incorrect spellings" testCaptcha it "should generate captcha of correct length" testGetCaptchaStr describe "help commands" $ do it "should not list audio command" testHelpNoAudio it "should reject audio command in DM" testAudioCommandInDM directoryProfile :: Profile directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing} mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = DirectoryOpts { coreOptions = testCoreOpts { dbOptions = (dbOptions testCoreOpts) #if defined(dbPostgres) {dbSchemaPrefix = "client_" <> serviceDbPrefix} #else {dbFilePrefix = ps serviceDbPrefix} #endif }, adminUsers = [], superUsers, ownersGroup, noAddress = False, blockedFragmentsFile = Nothing, blockedWordsFile = Nothing, blockedExtensionRules = Nothing, nameSpellingFile = Nothing, profileNameLimit = maxBound, captchaGenerator = Nothing, voiceCaptchaGenerator = Nothing, directoryLog = Just $ ps "directory_service.log", migrateDirectoryLog = Nothing, serviceName = "SimpleX Directory", runCLI = False, searchResults = 3, webFolder, testing = True } serviceDbPrefix :: FilePath serviceDbPrefix = "directory_service" viewName :: String -> String viewName = T.unpack . MD.viewName . T.pack testDirectoryService :: HasCallStack => TestParams -> IO () testDirectoryService ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink bob #> "@'SimpleX Directory' privacy" bob <# "'SimpleX Directory'> > privacy" bob <## " No groups found" -- putStrLn "*** create a group" bob ##> "/g PSA Privacy, Security & Anonymity" bob <## "group #PSA (Privacy, Security & Anonymity) is created" bob <## "to add members use /a PSA or /create link #PSA" bob ##> "/a PSA 'SimpleX Directory' member" bob <## "invitation to join the group #PSA sent to 'SimpleX Directory'" bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group" bob ##> "/mr PSA 'SimpleX Directory' admin" -- putStrLn "*** discover service joins group and creates the link for profile" bob <## "#PSA: you changed the role of 'SimpleX Directory' to admin" bob <# "'SimpleX Directory'> Joining the group PSA…" bob <## "#PSA: 'SimpleX Directory' joined the group" bob <# "'SimpleX Directory'> Joined the group PSA, creating the link…" bob <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online." bob <## "" bob <## "Please add it to the group welcome message." bob <## "For example, add:" welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob -- putStrLn "*** update profile without link" updateGroupProfile bob "Welcome!" bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." (superUser Thank you! The group link for ID 1 (PSA) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink (1 :: Int) -- putStrLn "*** update profile so that it still has link" let welcomeWithLink' = "Welcome! " <> welcomeWithLink updateGroupProfile bob welcomeWithLink' bob <# "'SimpleX Directory'> The group ID 1 (PSA) is updated!" bob <## "It is hidden from the directory until approved." superUser <# "'SimpleX Directory'> The group ID 1 (PSA) is updated." approvalRequested superUser welcomeWithLink' (2 :: Int) -- putStrLn "*** try approving with the old registration code" bob #> "@'SimpleX Directory' /approve 1:PSA 1" bob <# "'SimpleX Directory'> > /approve 1:PSA 1" bob <## " You are not allowed to use this command" superUser #> "@'SimpleX Directory' /approve 1:PSA 1" superUser <# "'SimpleX Directory'> > /approve 1:PSA 1" superUser <## " Incorrect approval code" -- putStrLn "*** update profile so that it has no link" updateGroupProfile bob "Welcome!" bob <# "'SimpleX Directory'> The group link for ID 1 (PSA) is removed from the welcome message." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (PSA), de-listed." superUser #> "@'SimpleX Directory' /approve 1:PSA 2" superUser <# "'SimpleX Directory'> > /approve 1:PSA 2" superUser <## " Error: the group ID 1 (PSA) is not pending approval." -- putStrLn "*** update profile so that it has link again" updateGroupProfile bob welcomeWithLink' bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink' (1 :: Int) superUser #> "@'SimpleX Directory' /pending" superUser <# "'SimpleX Directory'> > /pending" superUser <## " 1 registered group(s)" superUser <# "'SimpleX Directory'> 1. PSA (Privacy, Security & Anonymity)" superUser <## "Welcome message:" superUser <##. "Welcome! Link to join the group PSA: " superUser <## "Owner: bob" superUser <## "2 members" superUser <## "Status: pending admin approval" superUser <## "/'role 1', /'filter 1'" superUser #> "@'SimpleX Directory' /approve 1:PSA 1" superUser <# "'SimpleX Directory'> > /approve 1:PSA 1" superUser <## " Group approved!" bob <# "'SimpleX Directory'> The group ID 1 (PSA) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" bob <## "Supported commands:" bob <## "/'filter 1' - to configure anti-spam filter." bob <## "/'role 1' - to set default member role." bob <## "/'link 1' - to view/upgrade group link." search bob "privacy" welcomeWithLink' search bob "security" welcomeWithLink' cath `connectVia` dsLink search cath "privacy" welcomeWithLink' bob #> "@'SimpleX Directory' /exec /contacts" bob <# "'SimpleX Directory'> > /exec /contacts" bob <## " You are not allowed to use this command" superUser #> "@'SimpleX Directory' /exec /contacts" superUser <# "'SimpleX Directory'> > /exec /contacts" superUser <## " alice (Alice)" superUser <## "bob (Bob)" superUser <## "cath (Catherine)" where search u s welcome = do u #> ("@'SimpleX Directory' " <> s) u <# ("'SimpleX Directory'> > " <> s) u <## " Found 1 group(s)." u <# "'SimpleX Directory'> PSA (Privacy, Security & Anonymity)" u <## "Welcome message:" u <## welcome u <## "2 members" updateGroupProfile u welcome = do u ##> ("/set welcome #PSA " <> welcome) u <## "welcome message changed to:" u <## welcome approvalRequested su welcome grId = do su <# "'SimpleX Directory'> bob submitted the group ID 1:" su <## "PSA (Privacy, Security & Anonymity)" su <## "Welcome message:" su <## welcome su <## "2 members" su <## "" su <## "To approve send:" su <# ("'SimpleX Directory'> /approve 1:PSA " <> show grId) testSuspendResume :: HasCallStack => TestParams -> IO () testSuspendResume ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" superUser #> "@'SimpleX Directory' /suspend 1:privacy" superUser <# "'SimpleX Directory'> > /suspend 1:privacy" superUser <## " Group suspended!" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators." groupNotFound bob "privacy" superUser #> "@'SimpleX Directory' /resume 1:privacy" superUser <# "'SimpleX Directory'> > /resume 1:privacy" superUser <## " Group listing resumed!" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is listed in the directory again!" groupFound bob "privacy" superUser #> "@'SimpleX Directory' privacy" groupFoundN_ "" (Just 1) 2 superUser "privacy" superUser #> "@'SimpleX Directory' /link 1:privacy" superUser <# "'SimpleX Directory'> > /link 1:privacy" superUser <## " The link to join the group ID 1 (privacy):" superUser <##. "https://localhost/g#" superUser <## "New member role: member" -- get and change the link to the equivalent - should not ask to re-approve bob #> "@'SimpleX Directory' /link 1" bob <# "'SimpleX Directory'> > /link 1" bob <## " The link to join the group ID 1 (privacy):" gLink <- getTermLine bob gLink `shouldStartWith` "https://localhost/g#" bob <## "New member role: member" bob ##> "/show welcome #privacy" bob <## "Welcome message:" bob <## ("Link to join the group privacy: " <> gLink) bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true") bob <## "welcome message changed to:" bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true") bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "The group is listed in directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes." superUser <## "The group remained listed in directory." #if !defined(dbPostgres) -- upgrade link -- make it upgradeable first superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" superUser <# "'SimpleX Directory'> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" superUser <## "" bob #> "@'SimpleX Directory' /link 1" bob <# "'SimpleX Directory'> > /link 1" bob <## " The link to join the group ID 1 (privacy):" bob <##. "https://simplex.chat/contact#/" bob <## "New member role: member" bob <## "The link is being upgraded..." bob <# "'SimpleX Directory'> Please replace the old link in welcome message of your group ID 1 (privacy)" bob <## "If this is the only change, the group will remain listed in directory without re-approval." bob <## "" bob <## "The new link:" gLink' <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink') bob <## "welcome message changed to:" bob <## ("Link to join the group privacy: " <> gLink') bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "The group is listed in directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes." superUser <## "The group remained listed in directory." -- send message to group owner superUser #> "@'SimpleX Directory' /owner 1:privacy hello there" superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there" superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)" bob <# "'SimpleX Directory'> hello there" #endif testDeleteGroup :: HasCallStack => TestParams -> IO () testDeleteGroup ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" bob #> "@'SimpleX Directory' /delete 1:privacy" bob <# "'SimpleX Directory'> > /delete 1:privacy" bob <## " Your group privacy is deleted from the directory" groupNotFound bob "privacy" testDeleteGroupAdmin :: HasCallStack => TestParams -> IO () testDeleteGroupAdmin ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink registerGroupId superUser cath "security" "Security" 2 1 groupFound bob "privacy" groupFound bob "security" listUserGroup bob "privacy" "Privacy" listUserGroup cath "security" "Security" superUser #> "@'SimpleX Directory' /last" superUser <# "'SimpleX Directory'> > /last" superUser <## " 2 registered group(s)" memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active" memberGroupListing superUser cath 2 "security" "Security" 2 "active" -- trying to register group with the same name submitGroup bob "security" "Security" bob <# "'SimpleX Directory'> The group security (Security) is already listed in the directory, please choose another name." bob ##> "/d #security" bob <## "#security: you deleted the group" -- admin can delete the group superUser #> "@'SimpleX Directory' /delete 2:security" superUser <# "'SimpleX Directory'> > /delete 2:security" superUser <## " The group security is deleted from the directory" groupFound cath "privacy" listUserGroup bob "privacy" "Privacy" groupNotFound bob "security" sendListCommand cath 0 -- another user can register the group with the same name registerGroupId superUser bob "security" "Security" 4 2 testSetRole :: HasCallStack => TestParams -> IO () testSetRole ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" bob #> "@'SimpleX Directory' /role 1:privacy observer" bob <# "'SimpleX Directory'> > /role 1:privacy observer" bob <## " The initial member role for the group privacy is set to observer" bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" bob ##> "/ms #privacy" bob <## "bob (Bob): owner, you, created group" bob <## "'SimpleX Directory': admin, invited, connected" bob <## "cath (Catherine): observer, connected" cath ##> "#privacy hello" cath <## "#privacy: you don't have permission to send messages" testJoinGroup :: HasCallStack => TestParams -> IO () testJoinGroup ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "cath" cathProfile $ \cath -> withNewTestChat ps "dan" danProfile $ \dan -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink cath #> "@'SimpleX Directory' privacy" cath <# "'SimpleX Directory'> > privacy" cath <## " Found 1 group(s)." cath <# "'SimpleX Directory'> privacy (Privacy)" cath <## "Welcome message:" welcomeMsg <- getTermLine cath let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg cath <## "2 members" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" cath <### [ "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'", "use @'SimpleX Directory' to send messages", Predicate (\l -> l == welcomeMsg || dropTime_ l == Just ("#privacy 'SimpleX Directory'> " <> welcomeMsg) || dropTime_ l == Just ("#privacy 'SimpleX Directory_1'> " <> welcomeMsg)) ] cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" bob ##> "/create link #privacy" bobLink <- getGroupLink bob "privacy" GRMember True dan ##> ("/c " <> bobLink) dan <## "connection request sent!" concurrentlyN_ [ do bob <## "dan (Daniel): accepting request to join group #privacy..." bob <## "#privacy: dan joined the group", do dan <## "#privacy: joining the group..." dan <## "#privacy: you joined the group" dan <# ("#privacy bob> " <> welcomeMsg) dan <### [ "#privacy: member 'SimpleX Directory' is connected", "#privacy: member cath (Catherine) is connected" ], do cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)" cath <## "#privacy: new member dan is connected" ] testGroupNameWithSpaces :: HasCallStack => TestParams -> IO () testGroupNameWithSpaces ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroup superUser bob "Privacy & Security" "" groupFound bob "Privacy & Security" superUser #> "@'SimpleX Directory' /suspend 1:'Privacy & Security'" superUser <# "'SimpleX Directory'> > /suspend 1:'Privacy & Security'" superUser <## " Group suspended!" bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators." groupNotFound bob "privacy" superUser #> "@'SimpleX Directory' /resume 1:'Privacy & Security'" superUser <# "'SimpleX Directory'> > /resume 1:'Privacy & Security'" superUser <## " Group listing resumed!" bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is listed in the directory again!" groupFound bob "Privacy & Security" testSearchGroups :: HasCallStack => TestParams -> IO () testSearchGroups ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i connectUsers bob cath fullAddMember "MyGroup" "" bob cath GRMember joinGroup "MyGroup" cath bob cath <## "#MyGroup: member 'SimpleX Directory_1' is connected" cath <## "contact and member are merged: 'SimpleX Directory', #MyGroup 'SimpleX Directory_1'" cath <## "use @'SimpleX Directory' to send messages" cath #> "@'SimpleX Directory' MyGroup" cath <# "'SimpleX Directory'> > MyGroup" cath <## " Found 7 group(s), sending top 3." receivedGroup cath 0 3 receivedGroup cath 1 2 receivedGroup cath 2 2 cath <# "'SimpleX Directory'> Send /next for 4 more result(s)." cath #> "@'SimpleX Directory' /next" cath <# "'SimpleX Directory'> > /next" cath <## " Sending 3 more group(s)." receivedGroup cath 3 2 receivedGroup cath 4 2 receivedGroup cath 5 2 cath <# "'SimpleX Directory'> Send /next for 1 more result(s)." -- search of another user does not affect the search of the first user groupFound bob "Another" cath #> "@'SimpleX Directory' ." cath <# "'SimpleX Directory'> > ." cath <## " Sending 1 more group(s)." receivedGroup cath 6 2 cath #> "@'SimpleX Directory' /all" cath <# "'SimpleX Directory'> > /all" cath <## " 8 group(s) listed, sending top 3." receivedGroup cath 0 3 receivedGroup cath 1 2 receivedGroup cath 2 2 cath <# "'SimpleX Directory'> Send /next for 5 more result(s)." cath #> "@'SimpleX Directory' /new" cath <# "'SimpleX Directory'> > /new" cath <## " 8 group(s) listed, sending the most recent 3." receivedGroup cath 7 2 receivedGroup cath 6 2 receivedGroup cath 5 2 cath <# "'SimpleX Directory'> Send /next for 5 more result(s)." cath #> "@'SimpleX Directory' term3" cath <# "'SimpleX Directory'> > term3" cath <## " Found 3 group(s)." receivedGroup cath 4 2 receivedGroup cath 5 2 receivedGroup cath 6 2 cath #> "@'SimpleX Directory' term1" cath <# "'SimpleX Directory'> > term1" cath <## " Found 6 group(s), sending top 3." receivedGroup cath 1 2 receivedGroup cath 2 2 receivedGroup cath 3 2 cath <# "'SimpleX Directory'> Send /next for 3 more result(s)." cath #> "@'SimpleX Directory' ." cath <# "'SimpleX Directory'> > ." cath <## " Sending 3 more group(s)." receivedGroup cath 4 2 receivedGroup cath 5 2 receivedGroup cath 6 2 where groups :: [String] groups = [ "MyGroup", "MyGroup term1 1", "MyGroup term1 2", "MyGroup term1 term2", "MyGroup term1 term2 term3", "MyGroup term1 term2 term3 term4", "MyGroup term1 term2 term3 term4 term5", "Another" ] receivedGroup :: TestCC -> Int -> Int -> IO () receivedGroup u ix count = do u <#. ("'SimpleX Directory'> " <> groups !! ix) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") testInviteToOwnersGroup :: HasCallStack => TestParams -> IO () testInviteToOwnersGroup ps = withDirectoryServiceCfgOwnersGroup ps testCfg True Nothing $ \superUser dsLink -> withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroupId superUser bob "privacy" "Privacy" 2 1 bob <## "#owners: 'SimpleX Directory' invites you to join the group as member" bob <## "use /j owners to accept" superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners" bob ##> "/j owners" bob <## "#owners: you joined the group" bob <## "#owners: member alice (Alice) is connected" superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)" superUser <## "#owners: new member bob is connected" -- second group registerGroupId superUser bob "security" "Security" 3 2 superUser <## "Owner is already a member of owners' group" testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO () testDelistedOwnerLeaves ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath leaveGroup "privacy" bob cath <## "#privacy: bob left the group" bob <# "'SimpleX Directory'> You left the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner left)." cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO () testDelistedOwnerRemoved ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath removeMember "privacy" cath bob bob <# "'SimpleX Directory'> You are removed from the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner is removed)." cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO () testNotDelistedMemberLeaves ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath leaveGroup "privacy" cath bob <## "#privacy: cath left the group" (superUser "@'SimpleX Directory_1' privacy" groupFoundN_ "_1" Nothing 2 cath "privacy" testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO () testNotDelistedMemberRemoved ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath removeMember "privacy" bob cath (superUser "@'SimpleX Directory_1' privacy" groupFoundN_ "_1" Nothing 2 cath "privacy" testDelistedServiceRemoved :: HasCallStack => TestParams -> IO () testDelistedServiceRemoved ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath bob ##> "/rm #privacy 'SimpleX Directory'" bob <## "#privacy: you removed 'SimpleX Directory' from the group" cath <## "#privacy: bob removed 'SimpleX Directory' from the group" bob <# "'SimpleX Directory'> SimpleX Directory is removed from the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (directory service is removed)." cath `connectVia` dsLink groupNotFound_ "_1" cath "privacy" testDelistedGroupDeleted :: HasCallStack => TestParams -> IO () testDelistedGroupDeleted ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GROwner joinGroup "privacy" cath bob cath <## "#privacy: member 'SimpleX Directory_1' is connected" cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" cath <## "use @'SimpleX Directory' to send messages" bob ##> "/d #privacy" bob <## "#privacy: you deleted the group" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is deleted." bob <## "" bob <## "The group is no longer listed in the directory." cath <## "#privacy: bob deleted the group" cath <## "use /d #privacy to delete the local copy of the group" superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group is deleted)." groupNotFound cath "privacy" testDelistedRoleChanges :: HasCallStack => TestParams -> IO () testDelistedRoleChanges ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupFoundN 3 cath "privacy" -- de-listed if service role changed bob ##> "/mr privacy 'SimpleX Directory' member" bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" cath <## "#privacy: bob changed the role of 'SimpleX Directory' from admin to member" bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to member." bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (SimpleX Directory role is changed to member)." groupNotFound cath "privacy" -- re-listed if service role changed back without profile changes cath ##> "/mr privacy 'SimpleX Directory' admin" cath <## "#privacy: you changed the role of 'SimpleX Directory' to admin" bob <## "#privacy: cath changed the role of 'SimpleX Directory' from member to admin" bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is listed in the directory again." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (SimpleX Directory role is changed to admin)." groupFoundN 3 cath "privacy" -- de-listed if owner role changed cath ##> "/mr privacy bob admin" cath <## "#privacy: you changed the role of bob to admin" bob <## "#privacy: cath changed your role from owner to admin" bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (user role is set to admin)." groupNotFound cath "privacy" -- re-listed if owner role changed back without profile changes cath ##> "/mr privacy bob owner" cath <## "#privacy: you changed the role of bob to owner" bob <## "#privacy: cath changed your role from admin to owner" bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to owner." bob <## "" bob <## "The group is listed in the directory again." superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (user role is set to owner)." groupFoundN 3 cath "privacy" testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO () testNotDelistedMemberRoleChanged ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupFoundN 3 cath "privacy" bob ##> "/mr privacy cath member" bob <## "#privacy: you changed the role of cath to member" cath <## "#privacy: bob changed your role from owner to member" groupFoundN 3 cath "privacy" testNotSentApprovalBadRoles :: HasCallStack => TestParams -> IO () testNotSentApprovalBadRoles ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" bob ##> "/mr privacy 'SimpleX Directory' member" bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group" bob ##> "/mr privacy 'SimpleX Directory' admin" bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin" bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 groupNotFound cath "privacy" approveRegistration superUser bob "privacy" 1 groupFound cath "privacy" testNotApprovedBadRoles :: HasCallStack => TestParams -> IO () testNotApprovedBadRoles ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 bob ##> "/mr privacy 'SimpleX Directory' member" bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" let approve = "/approve 1:privacy 1" superUser #> ("@'SimpleX Directory' " <> approve) superUser <# ("'SimpleX Directory'> > " <> approve) superUser <## " Group is not approved: SimpleX Directory is not an admin." groupNotFound cath "privacy" bob ##> "/mr privacy 'SimpleX Directory' admin" bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin" bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 approveRegistration superUser bob "privacy" 1 groupFound cath "privacy" testRegOwnerChangedProfile :: HasCallStack => TestParams -> IO () testRegOwnerChangedProfile ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath bob ##> "/gp privacy privacy Privacy and Security" bob <## "description changed to: Privacy and Security" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "It is hidden from the directory until approved." cath <## "bob updated group #privacy:" cath <## "description changed to: Privacy and Security" cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" testAnotherOwnerChangedProfile :: HasCallStack => TestParams -> IO () testAnotherOwnerChangedProfile ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" cath ##> "/gp privacy privacy Privacy and Security" cath <## "description changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "description changed to: Privacy and Security" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound cath "privacy" superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO () testNotConnectedOwnerChangedProfile ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do withNewTestChat ps "dan" danProfile $ \dan -> do bob `connectVia` dsLink dan `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath ##> "/gp privacy privacy Privacy and Security" cath <## "description changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "description changed to: Privacy and Security" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound dan "privacy" superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 dan "privacy" testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO () testRegOwnerRemovedLink ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath bob ##> "/show welcome #privacy" bob <## "Welcome message:" welcomeWithLink <- getTermLine bob bob ##> "/set welcome #privacy Welcome!" bob <## "welcome message changed to:" bob <## "Welcome!" bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." cath <## "bob updated group #privacy:" cath <## "welcome message changed to:" cath <## "Welcome!" superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink bob ##> ("/set welcome #privacy " <> withChangedLink) bob <## "welcome message changed to:" bob <## withChangedLink bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "welcome message changed to:" cath <## withChangedLink reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" testAnotherOwnerRemovedLink :: HasCallStack => TestParams -> IO () testAnotherOwnerRemovedLink ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" cath <## "use @'SimpleX Directory' to send messages" bob ##> "/show welcome #privacy" bob <## "Welcome message:" welcomeWithLink <- getTermLine bob cath ##> "/set welcome #privacy Welcome!" cath <## "welcome message changed to:" cath <## "Welcome!" bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## "Welcome!" bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." groupNotFound cath "privacy" cath ##> ("/set welcome #privacy " <> welcomeWithLink) cath <## "welcome message changed to:" cath <## welcomeWithLink bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## welcomeWithLink bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO () testNotConnectedOwnerRemovedLink ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do withNewTestChat ps "dan" danProfile $ \dan -> do bob `connectVia` dsLink dan `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath bob ##> "/show welcome #privacy" bob <## "Welcome message:" welcomeWithLink <- getTermLine bob cath ##> "/set welcome #privacy Welcome!" cath <## "welcome message changed to:" cath <## "Welcome!" bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## "Welcome!" bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." groupNotFound dan "privacy" cath ##> ("/set welcome #privacy " <> welcomeWithLink) cath <## "welcome message changed to:" cath <## welcomeWithLink bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## welcomeWithLink -- bob <# "'SimpleX Directory'> The group link is added by another group member, your registration will not be processed." -- bob <## "" -- bob <## "Please update the group profile yourself." -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") -- bob <## "welcome message changed to:" -- bob <## (welcomeWithLink <> " - welcome!") bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." -- cath <## "bob updated group #privacy:" -- cath <## "welcome message changed to:" -- cath <## (welcomeWithLink <> " - welcome!") reapproveGroup 3 superUser bob groupFoundN 3 dan "privacy" testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO () testDuplicateAskConfirmation ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" _ <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink <- groupAccepted cath "privacy" groupNotFound bob "privacy" completeRegistrationId superUser cath "privacy" "Privacy" welcomeWithLink 2 1 groupFound bob "privacy" testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO () testDuplicateProhibitRegistration ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink groupFound cath "privacy" _ <- submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO () testDuplicateProhibitConfirmation ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO () testDuplicateProhibitWhenUpdated ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink' <- groupAccepted cath "privacy" groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" cath ##> ("/set welcome privacy " <> welcomeWithLink') cath <## "welcome message changed to:" cath <## welcomeWithLink' cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." cath ##> "/gp privacy security Security" cath <## "changed to #security (Security)" cath <# "'SimpleX Directory'> Thank you! The group link for ID 1 (security) is added to the welcome message." cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2 approveRegistrationId superUser cath "security" 2 1 groupFound bob "security" groupFound cath "security" testDuplicateProhibitApproval :: HasCallStack => TestParams -> IO () testDuplicateProhibitApproval ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink' <- groupAccepted cath "privacy" updateProfileWithLink cath "privacy" welcomeWithLink' 1 notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2 groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" -- fails at approval, as already listed let approve = "/approve 2:privacy 1" superUser #> ("@'SimpleX Directory' " <> approve) superUser <# ("'SimpleX Directory'> > " <> approve) superUser <## " The group ID 2 (privacy) is already listed in the directory." testListUserGroups :: HasCallStack => Bool -> TestParams -> IO () testListUserGroups promote ps = withDirectoryServiceCfgOwnersGroup ps testCfg False (Just "./tests/tmp/web") $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" checkListings ["privacy"] [] connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GRMember joinGroup "privacy" cath bob cath <## "#privacy: member 'SimpleX Directory_1' is connected" cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" cath <## "use @'SimpleX Directory' to send messages" registerGroupId superUser bob "security" "Security" 2 2 checkListings ["privacy", "security"] [] registerGroupId superUser cath "anonymity" "Anonymity" 3 1 checkListings ["privacy", "security", "anonymity"] [] listUserGroup cath "anonymity" "Anonymity" -- with de-listed group groupFound cath "anonymity" cath ##> "/mr anonymity 'SimpleX Directory' member" cath <## "#anonymity: you changed the role of 'SimpleX Directory' to member" cath <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (anonymity) is changed to member." cath <## "" cath <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)." checkListings ["privacy", "security"] [] groupNotFound cath "anonymity" listGroups superUser bob cath when promote $ do superUser #> "@'SimpleX Directory' /promote 1:privacy on" superUser <# "'SimpleX Directory'> > /promote 1:privacy on" superUser <## " Group promotion enabled." checkListings ["privacy", "security"] ["privacy"] bob ##> "/gp privacy privacy" bob <## "description removed" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "It is hidden from the directory until approved." cath <## "bob updated group #privacy:" cath <## "description removed" superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated." superUser <# "'SimpleX Directory'> bob submitted the group ID 1:" superUser <## "privacy" superUser <## "Welcome message:" superUser <##. "Link to join the group privacy: https://localhost/g#" superUser <## "3 members" superUser <## "" superUser <## "To approve send:" superUser <# "'SimpleX Directory'> /approve 1:privacy 1 promote=on" checkListings ["security"] [] superUser #> "@'SimpleX Directory' /approve 1:privacy 1" superUser <# "'SimpleX Directory'> > /approve 1:privacy 1" superUser <## " Group approved (promoted)!" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" bob <## "Supported commands:" bob <## "/'filter 1' - to configure anti-spam filter." bob <## "/'role 1' - to set default member role." bob <## "/'link 1' - to view/upgrade group link." checkListings ["privacy", "security"] ["privacy"] checkListings :: HasCallStack => [T.Text] -> [T.Text] -> IO () checkListings listed promoted = do threadDelay 100000 checkListing listingFileName listed checkListing promotedFileName promoted where checkListing f expected = do Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web/data" f map groupName gs `shouldBe` expected groupName DirectoryEntry {displayName} = displayName testCapthaScreening :: HasCallStack => TestParams -> IO () testCapthaScreening ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" -- check default role bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note -- enable captcha bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" -- connect with captcha screen _ <- join cath groupLink cath #> "#privacy (support) 123" -- sending incorrect captcha cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123" cath <## " Incorrect text, please try again." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath sendCaptcha cath captcha cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" cath #> "#privacy hello" bob <# "#privacy cath> hello" cath ##> "/l privacy" cath <## "#privacy: you left the group" cath <## "use /d #privacy to delete the group" bob <## "#privacy: cath left the group" cath ##> "/d #privacy" cath <## "#privacy: you deleted the group" -- change default role to observer bob #> "@'SimpleX Directory' /role 1 observer" bob <# "'SimpleX Directory'> > /role 1 observer" bob <## " The initial member role for the group privacy is set to observer" bob <## "" bob <##. "Please note: it applies only to members joining via this link: https://" -- connect with captcha screen again, as observer captcha' <- join cath groupLink sendCaptcha cath captcha' -- message from cath that left pastMember <- dropStrPrefix "#privacy: 'SimpleX Directory' forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath cath <# ("#privacy " <> pastMember <> "> hello [>>]") cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath_1 (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath_1 is connected" cath ##> "#privacy hello" cath <## "#privacy: you don't have permission to send messages" (bob "/ms privacy" cath <## "cath (Catherine): observer, you, connected" cath <## "'SimpleX Directory': admin, host, connected" cath <## "bob (Bob): owner, connected" cath <## (pastMember <> ": author, status unknown") where join cath groupLink = do cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath sendCaptcha cath captcha = do cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" testVoiceCaptchaScreening :: HasCallStack => TestParams -> IO () testVoiceCaptchaScreening ps@TestParams {tmpPath} = do let mockScript = tmpPath "mock_voice_gen.py" -- Mock script writes a dummy audio file, prints path and duration writeFile mockScript $ unlines [ "#!/usr/bin/env python3", "import os, tempfile", "out = os.environ.get('VOICE_CAPTCHA_OUT')", "if not out:", " fd, out = tempfile.mkstemp(suffix='.m4a')", " os.close(fd)", "open(out, 'wb').write(b'\\x00' * 100)", "print(out)", "print(5)" ] setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" -- get group link bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note -- enable captcha bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." cath <## "Send /audio to receive a voice captcha." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath -- cath requests audio captcha cath #> "#privacy (support) /audio" cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" cath <#. "#privacy (support) 'SimpleX Directory'> sends file " cath <##. "use /fr 1" -- cath sends /audio again, already enabled cath #> "#privacy (support) /audio" cath <# "#privacy (support) 'SimpleX Directory'!> > cath /audio" cath <## " Audio captcha is already enabled." -- send correct captcha sendCaptcha cath captcha cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" where sendCaptcha cath captcha = do cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" testVoiceCaptchaRetry :: HasCallStack => TestParams -> IO () testVoiceCaptchaRetry ps@TestParams {tmpPath} = do let mockScript = tmpPath "mock_voice_gen_retry.py" writeFile mockScript $ unlines [ "#!/usr/bin/env python3", "import os, tempfile", "out = os.environ.get('VOICE_CAPTCHA_OUT')", "if not out:", " fd, out = tempfile.mkstemp(suffix='.m4a')", " os.close(fd)", "open(out, 'wb').write(b'\\x00' * 100)", "print(out)", "print(5)" ] setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." cath <## "Send /audio to receive a voice captcha." _ <- getTermLine cath -- captcha image/text -- cath requests audio captcha cath #> "#privacy (support) /audio" cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" cath <#. "#privacy (support) 'SimpleX Directory'> sends file " cath <##. "use /fr 1" -- cath sends WRONG answer after switching to audio mode cath #> "#privacy (support) wrong_answer" cath <# "#privacy (support) 'SimpleX Directory'!> > cath wrong_answer" cath <## " Incorrect text, please try again." -- KEY ASSERTION: retry sends BOTH image and voice because captchaMode=CMAudio _ <- getTermLine cath -- captcha image/text cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" cath <#. "#privacy (support) 'SimpleX Directory'> sends file " cath <##. "use /fr 2" testVoiceCaptchaVoiceDisabled :: HasCallStack => TestParams -> IO () testVoiceCaptchaVoiceDisabled ps@TestParams {tmpPath} = do let mockScript = tmpPath "mock_voice_gen_vdisabled.py" writeFile mockScript $ unlines [ "#!/usr/bin/env python3", "import os, tempfile", "out = os.environ.get('VOICE_CAPTCHA_OUT')", "if not out:", " fd, out = tempfile.mkstemp(suffix='.m4a')", " os.close(fd)", "open(out, 'wb').write(b'\\x00' * 100)", "print(out)", "print(5)" ] setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" bob <## "Voice messages: off" -- cath (new client, supports v17 exemption) joins, /audio hint shown cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." cath <## "Send /audio to receive a voice captcha." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath -- voice captcha works despite voice being disabled (v17 host approval exemption) cath #> "#privacy (support) /audio" cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" cath <#. "#privacy (support) 'SimpleX Directory'> sends file " cath <##. "use /fr 1" sendCaptcha cath captcha cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" where sendCaptcha cath captcha = do cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" testVoiceCaptchaOldClient :: HasCallStack => TestParams -> IO () testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do let mockScript = tmpPath "mock_voice_gen_oldclient.py" writeFile mockScript $ unlines [ "#!/usr/bin/env python3", "import os, tempfile", "out = os.environ.get('VOICE_CAPTCHA_OUT')", "if not out:", " fd, out = tempfile.mkstemp(suffix='.m4a')", " os.close(fd)", "open(out, 'wb').write(b'\\x00' * 100)", "print(out)", "print(5)" ] setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChatCfg ps testCfgVPrev "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" bob <## "Voice messages: off" -- cath (old client, max version < v17) joins, /audio hint NOT shown cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath -- /audio unavailable: old client can't receive voice in voice-disabled group cath #> "#privacy (support) /audio" cath <# "#privacy (support) 'SimpleX Directory'!> > cath /audio" cath <## " Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha." -- text captcha still works sendCaptcha cath captcha cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" where sendCaptcha cath captcha = do cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO () withDirectoryServiceVoiceCaptcha ps voiceScript test = do dsLink <- withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser ds ##> "/ad" getContactLink ds True let opts = (mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing) {voiceCaptchaGenerator = Just voiceScript} runDirectory testCfg opts $ withTestChatCfg ps testCfg "super_user" $ \superUser -> do superUser <## "subscribed 1 connections on server localhost" test superUser dsLink testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do testListUserGroups False ps restoreDirectoryService ps 11 $ \superUser _dsLink -> withTestChat ps "bob" $ \bob -> withTestChat ps "cath" $ \cath -> do bob <## "subscribed 5 connections on server localhost" cath <## "subscribed 5 connections on server localhost" listGroups superUser bob cath groupFoundN 3 bob "privacy" groupFound bob "security" groupFoundN 3 cath "privacy" cath #> "@'SimpleX Directory' security" groupFoundN' 2 cath "security" testCaptcha :: HasCallStack => TestParams -> IO () testCaptcha _ps = do let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty" matchCaptchaStr captcha captcha `shouldBe` True matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () listGroups superUser bob cath = do sendListCommand bob 2 groupListing bob 1 "privacy" "Privacy" 3 "active" groupListing bob 2 "security" "Security" 2 "active" sendListCommand cath 1 groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed" -- superuser lists all groups bob #> "@'SimpleX Directory' /last" bob <# "'SimpleX Directory'> > /last" bob <## " You are not allowed to use this command" superUser #> "@'SimpleX Directory' /last" superUser <# "'SimpleX Directory'> > /last" superUser <## " 3 registered group(s)" memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active" memberGroupListing superUser bob 2 "security" "Security" 2 "active" memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" -- showing last 1 group superUser #> "@'SimpleX Directory' /last 1" superUser <# "'SimpleX Directory'> > /last 1" superUser <## " 3 registered group(s), showing the last 1" memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" listUserGroup :: HasCallStack => TestCC -> String -> String -> IO () listUserGroup u n fn = do sendListCommand u 1 groupListing u 1 n fn 2 "active" sendListCommand :: HasCallStack => TestCC -> Int -> IO () sendListCommand u count = do u #> "@'SimpleX Directory' /list" u <# "'SimpleX Directory'> > /list" u <## (" " <> show count <> " registered group(s)") groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO () groupListing u = groupListing_ u Nothing memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO () memberGroupListing su owner = groupListing_ su (Just owner) groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO () groupListing_ su owner_ gId n fn count status = do su <# ("'SimpleX Directory'> " <> show gId <> ". " <> n <> " (" <> fn <> ")") su <## "Welcome message:" su <##. ("Link to join the group " <> n <> ": ") forM_ owner_ $ \owner -> do ownerName <- userName owner su <## ("Owner: " <> ownerName) su <## (show count <> " members") su <## ("Status: " <> status) su <## ("/'role " <> show gId <> "', /'filter " <> show gId <> "'") reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO () reapproveGroup count superUser bob = do superUser <# "'SimpleX Directory'> bob submitted the group ID 1:" superUser <##. "privacy (" superUser <## "Welcome message:" superUser <##. "Link to join the group privacy: " superUser <## (show count <> " members") superUser <## "" superUser <## "To approve send:" superUser <# "'SimpleX Directory'> /approve 1:privacy 1" superUser #> "@'SimpleX Directory' /approve 1:privacy 1" superUser <# "'SimpleX Directory'> > /approve 1:privacy 1" superUser <## " Group approved!" bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" bob <## "Supported commands:" bob <## "/'filter 1' - to configure anti-spam filter." bob <## "/'role 1' - to set default member role." bob <## "/'link 1' - to view/upgrade group link." addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () addCathAsOwner bob cath = do connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GROwner joinGroup "privacy" cath bob cath <## "#privacy: member 'SimpleX Directory' is connected" withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO () withDirectoryService ps = withDirectoryServiceCfg ps testCfg withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO () withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False Nothing withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO () withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup webFolder test = do dsLink <- withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser when createOwnersGroup $ do superUser ##> "/g owners" superUser <## "group #owners is created" superUser <## "to add members use /a owners or /create link #owners" superUser ##> "/a owners 'SimpleX Directory' admin" superUser <## "invitation to join the group #owners sent to 'SimpleX Directory'" ds <## "#owners: alice invites you to join the group as admin" ds <## "use /j owners to accept" ds ##> "/j owners" ds <## "#owners: you joined the group" superUser <## "#owners: 'SimpleX Directory' joined the group" ds ##> "/ad" getContactLink ds True withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test restoreDirectoryService :: HasCallStack => TestParams -> Int -> (TestCC -> String -> IO ()) -> IO () restoreDirectoryService ps connCount test = do dsLink <- withTestChat ps serviceDbPrefix $ \ds -> do ds .<## ("subscribed " <> show connCount <> " connections on server localhost") ds ##> "/sa" dsLink <- getContactLink ds False ds <## "auto_accept on" pure dsLink withDirectory ps testCfg dsLink test withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO () withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False Nothing withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO () withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] (if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing) webFolder runDirectory cfg opts $ withTestChatCfg ps cfg "super_user" $ \superUser -> do if createOwnersGroup then superUser <## "subscribed 2 connections on server localhost" else superUser <## "subscribed 1 connections on server localhost" test superUser dsLink runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO () runDirectory cfg opts@DirectoryOpts {directoryLog} action = do st <- openDirectoryLog directoryLog t <- forkIO $ directoryService st opts cfg threadDelay 500000 action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1 registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO () registerGroupId su u n fn gId ugId = do submitGroup u n fn welcomeWithLink <- groupAccepted u n completeRegistrationId su u n fn welcomeWithLink gId ugId submitGroup :: TestCC -> String -> String -> IO () submitGroup u n fn = do u ##> ("/g " <> viewName n <> if null fn then "" else " " <> fn) u <## ("group #" <> viewName n <> (if null fn then "" else " (" <> fn <> ")") <> " is created") u <## ("to add members use /a " <> viewName n <> " or /create link #" <> viewName n) u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin") u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'") groupAccepted :: TestCC -> String -> IO String groupAccepted u n = do u <### [ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"), ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group") ] u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…") u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online." u <## "" u <## "Please add it to the group welcome message." u <## "For example, add:" dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () completeRegistration su u n fn welcomeWithLink gId = completeRegistrationId su u n fn welcomeWithLink gId gId completeRegistrationId :: TestCC -> TestCC -> String -> String -> String -> Int -> Int -> IO () completeRegistrationId su u n fn welcomeWithLink gId ugId = do updateProfileWithLink u n welcomeWithLink ugId notifySuperUser su u n fn welcomeWithLink gId approveRegistrationId su u n gId ugId updateProfileWithLink :: TestCC -> String -> String -> Int -> IO () updateProfileWithLink u n welcomeWithLink ugId = do u ##> ("/set welcome " <> viewName n <> " " <> welcomeWithLink) u <## "welcome message changed to:" u <## welcomeWithLink u <# ("'SimpleX Directory'> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") u <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () notifySuperUser su u n fn welcomeWithLink gId = do uName <- userName u su <# ("'SimpleX Directory'> " <> uName <> " submitted the group ID " <> show gId <> ":") su <## (n <> if null fn then "" else " (" <> fn <> ")") su <## "Welcome message:" su <## welcomeWithLink su .<## "members" su <## "" su <## "To approve send:" let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1" su <# ("'SimpleX Directory'> " <> approve) approveRegistration :: TestCC -> TestCC -> String -> Int -> IO () approveRegistration su u n gId = approveRegistrationId su u n gId gId approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO () approveRegistrationId su u n gId ugId = do let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1" su #> ("@'SimpleX Directory' " <> approve) su <# ("'SimpleX Directory'> > " <> approve) su <## " Group approved!" u <# ("'SimpleX Directory'> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!") u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." u <## "" u <## "Supported commands:" u <## ("/'filter " <> show ugId <> "' - to configure anti-spam filter.") u <## ("/'role " <> show ugId <> "' - to set default member role.") u <## ("/'link " <> show ugId <> "' - to view/upgrade group link.") connectVia :: TestCC -> String -> IO () u `connectVia` dsLink = do u ##> ("/c " <> dsLink) u <## "connection request sent!" u .<## ": contact is connected" u .<# "> Welcome to SimpleX Directory!" u <## "" u <## "🔍 Send search string to find groups - try security." u <## "/help - how to submit your group." u <## "/new - recent groups." u <## "" u <## "[Directory rules](https://simplex.chat/docs/directory.html)." joinGroup :: String -> TestCC -> TestCC -> IO () joinGroup gName member host = do let gn = "#" <> gName memberName <- userName member hostName <- userName host member ##> ("/j " <> gName) member <## (gn <> ": you joined the group") member <#. (gn <> " " <> hostName <> "> Link to join the group " <> gName <> ": ") host <## (gn <> ": " <> memberName <> " joined the group") leaveGroup :: String -> TestCC -> IO () leaveGroup gName member = do let gn = "#" <> gName member ##> ("/l " <> gName) member <## (gn <> ": you left the group") member <## ("use /d " <> gn <> " to delete the group") removeMember :: String -> TestCC -> TestCC -> IO () removeMember gName admin removed = do let gn = "#" <> gName adminName <- userName admin removedName <- userName removed admin ##> ("/rm " <> gName <> " " <> removedName) admin <## (gn <> ": you removed " <> removedName <> " from the group") removed <## (gn <> ": " <> adminName <> " removed you from the group") removed <## ("use /d " <> gn <> " to delete the group") groupFound :: TestCC -> String -> IO () groupFound = groupFoundN 2 groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do u #> ("@'SimpleX Directory' " <> name) groupFoundN' count u name groupFoundN' :: Int -> TestCC -> String -> IO () groupFoundN' = groupFoundN_ "" Nothing groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO () groupFoundN_ suffix shownId_ count u name = do u <# ("'SimpleX Directory" <> suffix <> "'> > " <> name) u <## " Found 1 group(s)." u <#. ("'SimpleX Directory" <> suffix <> "'> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") groupNotFound :: TestCC -> String -> IO () groupNotFound = groupNotFound_ "" groupNotFound_ :: String -> TestCC -> String -> IO () groupNotFound_ suffix u s = do u #> ("@'SimpleX Directory" <> suffix <> "' " <> s) u <# ("'SimpleX Directory" <> suffix <> "'> > " <> s) u <## " No groups found" testCaptchaTooManyAttempts :: HasCallStack => TestParams -> IO () testCaptchaTooManyAttempts ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." _ <- getTermLine cath forM_ [1 :: Int .. 4] $ \i -> do cath #> "#privacy (support) wrong" cath <# "#privacy (support) 'SimpleX Directory'!> > cath wrong" if i == 4 then cath <## " Incorrect text, please try again - this is your last attempt." else cath <## " Incorrect text, please try again." _ <- getTermLine cath pure () cath #> "#privacy (support) wrong" cath <# "#privacy (support) 'SimpleX Directory'> Too many failed attempts, you can't join group." -- member removal produces multiple messages _ <- getTermLine cath _ <- getTermLine cath _ <- getTermLine cath pure () testCaptchaUnknownCommand :: HasCallStack => TestParams -> IO () testCaptchaUnknownCommand ps = withDirectoryService ps $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note bob #> "@'SimpleX Directory' /filter 1 captcha" bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" bob <## "/'filter 1 name' - enable name filter" bob <## "/'filter 1 name captcha' - enable both" bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." _ <- getTermLine cath cath #> "#privacy (support) /help" cath <# "#privacy (support) 'SimpleX Directory'!> > cath /help" cath <## " Unknown command, please enter captcha text." testHelpNoAudio :: HasCallStack => TestParams -> IO () testHelpNoAudio ps = withDirectoryService ps $ \_ dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink -- commands help should not mention /audio bob #> "@'SimpleX Directory' /help commands" bob <# "'SimpleX Directory'> /'help commands' - receive this help message." bob <## "/help - how to register your group to be added to directory." bob <## "/list - list the groups you registered." bob <## "`/role ` - view and set default member role for your group." bob <## "`/filter ` - view and set spam filter settings for group." bob <## "`/link ` - view and upgrade group link." bob <## "`/delete :` - remove the group you submitted from directory, with ID and name as shown by /list command." bob <## "" bob <## "To search for groups, send the search text." testAudioCommandInDM :: HasCallStack => TestParams -> IO () testAudioCommandInDM ps = withDirectoryService ps $ \_ dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink bob #> "@'SimpleX Directory' /audio" bob <# "'SimpleX Directory'> > /audio" bob <## " Unknown command" testGetCaptchaStr :: HasCallStack => TestParams -> IO () testGetCaptchaStr _ps = do s0 <- getCaptchaStr 0 "" s0 `shouldBe` "" s7 <- getCaptchaStr 7 "" length s7 `shouldBe` 7 all (`elem` ("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" :: String)) s7 `shouldBe` True