Files
simplex-chat/tests/ChatClient.hs
T

399 lines
16 KiB
Haskell
Raw Normal View History

2021-07-07 22:46:38 +01:00
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
2021-07-07 22:46:38 +01:00
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
2023-01-31 11:07:48 +00:00
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
2021-07-07 22:46:38 +01:00
2023-08-25 04:56:37 +08:00
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
2021-07-07 22:46:38 +01:00
module ChatClient where
2023-01-31 11:07:48 +00:00
import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay)
2021-07-07 22:46:38 +01:00
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception (bracket, bracket_)
2023-08-25 04:56:37 +08:00
import Control.Monad
2021-07-07 22:46:38 +01:00
import Control.Monad.Except
2022-10-14 13:06:33 +01:00
import Data.Functor (($>))
2022-04-25 16:30:21 +01:00
import Data.List (dropWhileEnd, find)
import Data.Maybe (fromJust, isNothing)
import qualified Data.Text as T
import Network.Socket
2021-07-07 22:46:38 +01:00
import Simplex.Chat
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..))
import Simplex.Chat.Core
2021-07-07 22:46:38 +01:00
import Simplex.Chat.Options
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Terminal
import Simplex.Chat.Terminal.Output (newChatTerminal)
2023-01-13 13:54:07 +04:00
import Simplex.Chat.Types (AgentUserId (..), Profile, User (..))
import Simplex.FileTransfer.Description (kb, mb)
import Simplex.FileTransfer.Server (runXFTPServerBlocking)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration)
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..))
import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig)
import Simplex.Messaging.Server (runSMPServerBlocking)
import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Transport
2023-07-03 20:13:00 +04:00
import Simplex.Messaging.Transport.Server (defaultTransportServerConfig)
import Simplex.Messaging.Version
2023-01-31 11:07:48 +00:00
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
import System.FilePath ((</>))
2021-07-07 22:46:38 +01:00
import qualified System.Terminal as C
import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal)
import System.Timeout (timeout)
2023-01-31 11:07:48 +00:00
import Test.Hspec (Expectation, HasCallStack, shouldReturn)
2021-07-07 22:46:38 +01:00
testDBPrefix :: FilePath
testDBPrefix = "tests/tmp/test"
2021-07-07 22:46:38 +01:00
serverPort :: ServiceName
2023-01-31 11:07:48 +00:00
serverPort = "7001"
2022-06-06 16:23:47 +01:00
testOpts :: ChatOpts
testOpts =
2021-07-07 22:46:38 +01:00
ChatOpts
{ coreOptions =
CoreChatOpts
{ dbFilePrefix = undefined,
dbKey = "",
-- dbKey = "this is a pass-phrase to encrypt the database",
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"],
networkConfig = defaultNetworkConfig,
logLevel = CLLImportant,
logConnections = False,
logServerHosts = False,
logAgent = Nothing,
logFile = Nothing,
tbqSize = 16
},
chatCmd = "",
chatCmdDelay = 3,
2022-06-06 16:23:47 +01:00
chatServerPort = Nothing,
2022-12-26 22:24:34 +00:00
optFilesFolder = Nothing,
2023-05-15 12:28:53 +02:00
showReactions = True,
allowInstantFiles = True,
2023-06-16 13:43:06 +01:00
autoAcceptFileSize = 0,
muteNotifications = True,
2022-06-06 16:23:47 +01:00
maintenance = False
2021-07-07 22:46:38 +01:00
}
getTestOpts :: Bool -> String -> ChatOpts
getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = (coreOptions testOpts) {dbKey}}
2021-07-07 22:46:38 +01:00
termSettings :: VirtualTerminalSettings
termSettings =
VirtualTerminalSettings
{ virtualType = "xterm",
virtualWindowSize = pure C.Size {height = 24, width = 1000},
virtualEvent = retry,
virtualInterrupt = retry
}
data TestCC = TestCC
{ chatController :: ChatController,
virtualTerminal :: VirtualTerminal,
chatAsync :: Async (),
termAsync :: Async (),
2023-05-24 16:14:41 +04:00
termQ :: TQueue String,
printOutput :: Bool
}
2021-07-07 22:46:38 +01:00
aCfg :: AgentConfig
aCfg = (agentConfig defaultChatConfig) {tbqSize = 16}
testAgentCfg :: AgentConfig
2023-03-30 18:36:39 +04:00
testAgentCfg =
aCfg
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000},
xftpNotifyErrsOnRetry = False
}
testCfg :: ChatConfig
testCfg =
defaultChatConfig
{ agentConfig = testAgentCfg,
2023-07-13 23:48:25 +01:00
showReceipts = False,
testView = True,
tbqSize = 16,
xftpFileConfig = Nothing
}
testAgentCfgV1 :: AgentConfig
testAgentCfgV1 =
testAgentCfg
{ smpClientVRange = mkVersionRange 1 1,
smpAgentVRange = mkVersionRange 1 1,
2023-07-03 20:13:00 +04:00
smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange 1 1}
}
testCfgV1 :: ChatConfig
testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
testCfgCreateGroupDirect :: ChatConfig
testCfgCreateGroupDirect =
mkCfgCreateGroupDirect testCfg
mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig
mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange}
groupCreateDirectVRange :: VersionRange
groupCreateDirectVRange = mkVersionRange 1 1
2023-01-31 11:07:48 +00:00
createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do
Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey MCError
2023-01-13 13:54:07 +04:00
Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile True
startTestChat_ db cfg opts user
2022-04-25 16:30:21 +01:00
2023-01-31 11:07:48 +00:00
startTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> IO TestCC
startTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix = do
Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey MCError
Just user <- find activeUser <$> withTransaction chatStore getUsers
startTestChat_ db cfg opts user
2022-04-25 16:30:21 +01:00
startTestChat_ :: ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC
startTestChat_ db cfg opts user = do
2021-07-07 22:46:38 +01:00
t <- withVirtualTerminal termSettings pure
ct <- newChatTerminal t opts
cc <- newChatController db (Just user) cfg opts
2022-06-06 16:23:47 +01:00
chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct
atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry
termQ <- newTQueueIO
termAsync <- async $ readTerminalOutput t termQ
2023-05-24 16:14:41 +04:00
pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput = False}
2022-04-25 16:30:21 +01:00
stopTestChat :: TestCC -> IO ()
stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
stopChatController cc
2022-04-25 16:30:21 +01:00
uninterruptibleCancel termAsync
uninterruptibleCancel chatAsync
2023-01-31 11:07:48 +00:00
threadDelay 200000
2022-04-25 16:30:21 +01:00
2023-01-31 11:07:48 +00:00
withNewTestChat :: HasCallStack => FilePath -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a
withNewTestChat tmp = withNewTestChatCfgOpts tmp testCfg testOpts
2023-01-31 11:07:48 +00:00
withNewTestChatV1 :: HasCallStack => FilePath -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a
withNewTestChatV1 tmp = withNewTestChatCfg tmp testCfgV1
2023-01-31 11:07:48 +00:00
withNewTestChatCfg :: HasCallStack => FilePath -> ChatConfig -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a
withNewTestChatCfg tmp cfg = withNewTestChatCfgOpts tmp cfg testOpts
2022-06-06 16:23:47 +01:00
2023-01-31 11:07:48 +00:00
withNewTestChatOpts :: HasCallStack => FilePath -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a
withNewTestChatOpts tmp = withNewTestChatCfgOpts tmp testCfg
2023-01-31 11:07:48 +00:00
withNewTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a
withNewTestChatCfgOpts tmp cfg opts dbPrefix profile runTest =
2022-10-14 13:06:33 +01:00
bracket
2023-01-31 11:07:48 +00:00
(createTestChat tmp cfg opts dbPrefix profile)
2022-10-14 13:06:33 +01:00
stopTestChat
(\cc -> runTest cc >>= ((cc <// 100000) $>))
2023-01-31 11:07:48 +00:00
withTestChatV1 :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatV1 tmp = withTestChatCfg tmp testCfgV1
2022-04-25 16:30:21 +01:00
2023-01-31 11:07:48 +00:00
withTestChat :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChat tmp = withTestChatCfgOpts tmp testCfg testOpts
2023-01-31 11:07:48 +00:00
withTestChatCfg :: HasCallStack => FilePath -> ChatConfig -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatCfg tmp cfg = withTestChatCfgOpts tmp cfg testOpts
2022-06-06 16:23:47 +01:00
2023-01-31 11:07:48 +00:00
withTestChatOpts :: HasCallStack => FilePath -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatOpts tmp = withTestChatCfgOpts tmp testCfg
2023-01-31 11:07:48 +00:00
withTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a
withTestChatCfgOpts tmp cfg opts dbPrefix = bracket (startTestChat tmp cfg opts dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc)
2022-04-25 16:30:21 +01:00
-- enable output for specific chat controller, use like this:
-- withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do ...
2023-05-24 16:14:41 +04:00
withTestOutput :: HasCallStack => TestCC -> (HasCallStack => TestCC -> IO a) -> IO a
withTestOutput cc runTest = runTest cc {printOutput = True}
readTerminalOutput :: VirtualTerminal -> TQueue String -> IO ()
readTerminalOutput t termQ = do
let w = virtualWindow t
winVar <- atomically $ newTVar . init =<< readTVar w
forever . atomically $ do
win <- readTVar winVar
win' <- init <$> readTVar w
if win' == win
then retry
else do
let diff = getDiff win' win
forM_ diff $ writeTQueue termQ
writeTVar winVar win'
where
getDiff :: [String] -> [String] -> [String]
getDiff win win' = getDiff_ 1 (length win) win win'
getDiff_ :: Int -> Int -> [String] -> [String] -> [String]
getDiff_ n len win' win =
let diff = drop (len - n) win'
in if drop n win <> diff == win'
then map (dropWhileEnd (== ' ')) diff
else getDiff_ (n + 1) len win' win
2021-07-07 22:46:38 +01:00
withTmpFiles :: IO () -> IO ()
withTmpFiles =
bracket_
(createDirectoryIfMissing False "tests/tmp")
2023-01-31 11:07:48 +00:00
(removeDirectoryRecursive "tests/tmp")
2023-01-31 11:07:48 +00:00
testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> FilePath -> IO ()
testChatN cfg opts ps test tmp = do
2022-04-25 16:30:21 +01:00
tcs <- getTestCCs (zip ps [1 ..]) []
test tcs
concurrentlyN_ $ map (<// 100000) tcs
2022-04-25 16:30:21 +01:00
concurrentlyN_ $ map stopTestChat tcs
where
2022-04-25 16:30:21 +01:00
getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC]
getTestCCs [] tcs = pure tcs
2023-01-31 11:07:48 +00:00
getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat tmp cfg opts (show db) p <*> getTestCCs envs' tcs
2023-01-31 11:07:48 +00:00
(<//) :: HasCallStack => TestCC -> Int -> Expectation
2022-02-02 23:50:43 +04:00
(<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing
2023-01-31 11:07:48 +00:00
getTermLine :: HasCallStack => TestCC -> IO String
getTermLine cc =
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
Just s -> do
2023-05-24 16:14:41 +04:00
-- remove condition to always echo virtual terminal
when (printOutput cc) $ do
-- when True $ do
2023-05-24 16:14:41 +04:00
name <- userName cc
putStrLn $ name <> ": " <> s
pure s
_ -> error "no output for 5 seconds"
userName :: TestCC -> IO [Char]
2023-05-24 16:14:41 +04:00
userName (TestCC ChatController {currentUser} _ _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser
2023-01-31 11:07:48 +00:00
testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChat2 = testChatCfgOpts2 testCfg testOpts
2023-01-31 11:07:48 +00:00
testChatCfg2 :: HasCallStack => ChatConfig -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatCfg2 cfg = testChatCfgOpts2 cfg testOpts
2022-06-06 16:23:47 +01:00
2023-01-31 11:07:48 +00:00
testChatOpts2 :: HasCallStack => ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatOpts2 = testChatCfgOpts2 testCfg
2023-01-31 11:07:48 +00:00
testChatCfgOpts2 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_
where
test_ :: HasCallStack => [TestCC] -> IO ()
test_ [tc1, tc2] = test tc1 tc2
test_ _ = error "expected 2 chat clients"
2023-01-31 11:07:48 +00:00
testChat3 :: HasCallStack => Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChat3 = testChatCfgOpts3 testCfg testOpts
2023-01-31 11:07:48 +00:00
testChatCfg3 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatCfg3 cfg = testChatCfgOpts3 cfg testOpts
2023-01-31 11:07:48 +00:00
testChatCfgOpts3 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_
where
test_ :: HasCallStack => [TestCC] -> IO ()
test_ [tc1, tc2, tc3] = test tc1 tc2 tc3
test_ _ = error "expected 3 chat clients"
2023-01-31 11:07:48 +00:00
testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChat4 = testChatCfg4 testCfg
testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_
where
test_ :: HasCallStack => [TestCC] -> IO ()
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4
test_ _ = error "expected 4 chat clients"
concurrentlyN_ :: [IO a] -> IO ()
concurrentlyN_ = mapConcurrently_ id
serverCfg :: ServerConfig
serverCfg =
ServerConfig
{ transports = [(serverPort, transport @TLS)],
tbqSize = 1,
-- serverTbqSize = 1,
2022-04-26 12:52:41 +04:00
msgQueueQuota = 16,
queueIdBytes = 12,
msgIdBytes = 6,
2022-04-21 20:04:22 +01:00
storeLogFile = Nothing,
2022-06-16 20:00:51 +01:00
storeMsgsFile = Nothing,
2022-04-21 20:04:22 +01:00
allowNewQueues = True,
2022-12-02 15:01:26 +00:00
-- server password is disabled as otherwise v1 tests fail
newQueueBasicAuth = Nothing, -- Just "server_password",
2022-04-30 12:47:50 +01:00
messageExpiration = Just defaultMessageExpiration,
inactiveClientExpiration = Just defaultInactiveClientExpiration,
caCertificateFile = "tests/fixtures/tls/ca.crt",
privateKeyFile = "tests/fixtures/tls/server.key",
2022-05-09 18:53:39 +04:00
certificateFile = "tests/fixtures/tls/server.crt",
2023-01-31 11:07:48 +00:00
logStatsInterval = Nothing,
logStatsStartTime = 0,
2022-07-17 15:51:17 +01:00
serverStatsLogFile = "tests/smp-server-stats.daily.log",
serverStatsBackupFile = Nothing,
smpServerVRange = supportedSMPServerVRange,
transportConfig = defaultTransportServerConfig,
controlPort = Nothing
}
2023-01-31 11:07:48 +00:00
withSmpServer :: IO () -> IO ()
withSmpServer = serverBracket (`runSMPServerBlocking` serverCfg)
xftpTestPort :: ServiceName
xftpTestPort = "7002"
xftpServerFiles :: FilePath
xftpServerFiles = "tests/tmp/xftp-server-files"
xftpServerConfig :: XFTPServerConfig
xftpServerConfig =
XFTPServerConfig
{ xftpPort = xftpTestPort,
fileIdSize = 16,
2023-03-22 18:48:38 +04:00
storeLogFile = Just "tests/tmp/xftp-server-store.log",
filesPath = xftpServerFiles,
fileSizeQuota = Nothing,
allowedChunkSizes = [kb 128, kb 256, mb 1, mb 4],
allowNewFiles = True,
newFileBasicAuth = Nothing,
fileExpiration = Just defaultFileExpiration,
caCertificateFile = "tests/fixtures/tls/ca.crt",
privateKeyFile = "tests/fixtures/tls/server.key",
certificateFile = "tests/fixtures/tls/server.crt",
logStatsInterval = Nothing,
logStatsStartTime = 0,
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
serverStatsBackupFile = Nothing,
2023-07-03 20:13:00 +04:00
transportConfig = defaultTransportServerConfig
}
withXFTPServer :: IO () -> IO ()
withXFTPServer = withXFTPServer' xftpServerConfig
withXFTPServer' :: XFTPServerConfig -> IO () -> IO ()
withXFTPServer' cfg =
serverBracket
( \started -> do
createDirectoryIfMissing False xftpServerFiles
runXFTPServerBlocking started cfg
)
2023-01-31 11:07:48 +00:00
serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO ()
serverBracket server f = do
started <- newEmptyTMVarIO
bracket
2023-01-31 11:07:48 +00:00
(forkIOWithUnmask ($ server started))
(\t -> killThread t >> waitFor started "stop")
(\_ -> waitFor started "start" >> f)
where
waitFor started s =
5000000 `timeout` atomically (takeTMVar started) >>= \case
Nothing -> error $ "server did not " <> s
_ -> pure ()