mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into ep/agent-user
This commit is contained in:
@@ -4,6 +4,7 @@ import ChatTests.ChatList
|
||||
import ChatTests.Direct
|
||||
import ChatTests.Files
|
||||
import ChatTests.Groups
|
||||
import ChatTests.Local
|
||||
import ChatTests.Profiles
|
||||
import Test.Hspec
|
||||
|
||||
@@ -11,6 +12,7 @@ chatTests :: SpecWith FilePath
|
||||
chatTests = do
|
||||
describe "direct tests" chatDirectTests
|
||||
describe "group tests" chatGroupTests
|
||||
describe "local chats tests" chatLocalChatsTests
|
||||
describe "file tests" chatFileTests
|
||||
describe "profile tests" chatProfileTests
|
||||
describe "chat list pagination tests" chatListTests
|
||||
|
||||
@@ -191,17 +191,23 @@ testPaginationAllChatTypes =
|
||||
connectUsers alice dan
|
||||
alice <##> dan
|
||||
|
||||
ts6 <- iso8601Show <$> getCurrentTime
|
||||
_ts6 <- iso8601Show <$> getCurrentTime
|
||||
|
||||
getChats_ alice "count=10" [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice "count=3" [("@dan", "hey"), ("#team", ""), (":3", "")]
|
||||
-- * (notes)
|
||||
createCCNoteFolder alice
|
||||
alice /* "psst"
|
||||
|
||||
ts7 <- iso8601Show <$> getCurrentTime
|
||||
|
||||
getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "")]
|
||||
getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("<@cath", "")]
|
||||
getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", ""), (":3", "")]
|
||||
getChats_ alice ("after=" <> ts3 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", "")]
|
||||
getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", ""), (":3", "")]
|
||||
getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice ("after=" <> ts1 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice ("before=" <> ts6 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice ("after=" <> ts6 <> " count=10") []
|
||||
getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
|
||||
getChats_ alice ("after=" <> ts7 <> " count=10") []
|
||||
getChats_ alice ("before=" <> ts1 <> " count=10") []
|
||||
|
||||
let queryFavorite = "{\"type\": \"filters\", \"favorite\": true, \"unread\": false}"
|
||||
|
||||
@@ -1149,7 +1149,7 @@ testSubscribeAppNSE tmp =
|
||||
alice ##> "/_app suspend 1"
|
||||
alice <## "ok"
|
||||
alice <## "chat suspended"
|
||||
nseAlice ##> "/_start subscribe=off expire=off xftp=off"
|
||||
nseAlice ##> "/_start main=off"
|
||||
nseAlice <## "chat started"
|
||||
nseAlice ##> "/ad"
|
||||
cLink <- getContactLink nseAlice True
|
||||
@@ -1386,14 +1386,14 @@ testMultipleUserAddresses =
|
||||
cLinkAlisa <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLinkAlisa)
|
||||
alice <#? bob
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", "")])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("*", "")])
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alisa: contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature)])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("*", "")])
|
||||
alice <##> bob
|
||||
|
||||
bob #> "@alice hey alice"
|
||||
@@ -1424,7 +1424,7 @@ testMultipleUserAddresses =
|
||||
(cath <## "alisa: contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey")])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("*", "")])
|
||||
alice <##> cath
|
||||
|
||||
-- first user doesn't have cath as contact
|
||||
|
||||
@@ -132,6 +132,7 @@ chatGroupTests = do
|
||||
it "deleted message is not included" testGroupHistoryDeletedMessage
|
||||
it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage
|
||||
it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage
|
||||
it "unknown member messages are processed" testGroupHistoryUnknownMember
|
||||
where
|
||||
_0 = supportedChatVRange -- don't create direct connections
|
||||
_1 = groupCreateDirectVRange
|
||||
@@ -5179,3 +5180,71 @@ testGroupHistoryWelcomeMessage =
|
||||
[alice, cath] *<# "#team bob> 2"
|
||||
cath #> "#team 3"
|
||||
[alice, bob] *<# "#team cath> 3"
|
||||
|
||||
testGroupHistoryUnknownMember :: HasCallStack => FilePath -> IO ()
|
||||
testGroupHistoryUnknownMember =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
alice #> "#team hi from alice"
|
||||
[bob, cath] *<# "#team alice> hi from alice"
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
bob #> "#team hi from bob"
|
||||
[alice, cath] *<# "#team bob> hi from bob"
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
cath #> "#team hi from cath"
|
||||
[alice, bob] *<# "#team cath> hi from cath"
|
||||
|
||||
bob ##> "/l team"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## "#team: you left the group"
|
||||
bob <## "use /d #team to delete the group",
|
||||
alice <## "#team: bob left the group",
|
||||
cath <## "#team: bob left the group"
|
||||
]
|
||||
|
||||
connectUsers alice dan
|
||||
addMember "team" alice dan GRAdmin
|
||||
dan ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: dan joined the group",
|
||||
dan
|
||||
<### [ "#team: you joined the group",
|
||||
WithTime "#team alice> hi from alice [>>]",
|
||||
StartsWith "#team: alice forwarded a message from an unknown member, creating unknown member record",
|
||||
EndsWith "hi from bob [>>]",
|
||||
WithTime "#team cath> hi from cath [>>]",
|
||||
"#team: member cath (Catherine) is connected"
|
||||
],
|
||||
do
|
||||
cath <## "#team: alice added dan (Daniel) to the group (connecting...)"
|
||||
cath <## "#team: new member dan is connected"
|
||||
]
|
||||
|
||||
dan ##> "/_get chat #1 count=100"
|
||||
r <- chat <$> getTermLine dan
|
||||
r `shouldContain` [(0, "hi from alice"), (0, "hi from bob"), (0, "hi from cath")]
|
||||
|
||||
dan ##> "/ms team"
|
||||
dan
|
||||
<### [ "dan (Daniel): admin, you, connected",
|
||||
"alice (Alice): owner, host, connected",
|
||||
"cath (Catherine): admin, connected",
|
||||
EndsWith "author, status unknown"
|
||||
]
|
||||
|
||||
-- message delivery works after sending history
|
||||
alice #> "#team 1"
|
||||
[cath, dan] *<# "#team alice> 1"
|
||||
cath #> "#team 2"
|
||||
[alice, dan] *<# "#team cath> 2"
|
||||
dan #> "#team 3"
|
||||
[alice, cath] *<# "#team dan> 3"
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PostfixOperators #-}
|
||||
|
||||
module ChatTests.Local where
|
||||
|
||||
import ChatClient
|
||||
import ChatTests.ChatList (getChats_)
|
||||
import ChatTests.Utils
|
||||
import Data.Time (getCurrentTime)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), defaultInlineFilesConfig)
|
||||
import System.Directory (copyFile, doesFileExist)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
import UnliftIO.Async (concurrently_)
|
||||
|
||||
chatLocalChatsTests :: SpecWith FilePath
|
||||
chatLocalChatsTests = do
|
||||
describe "note folders" $ do
|
||||
it "create folders, add notes, read, search" testNotes
|
||||
it "switch users" testUserNotes
|
||||
it "preview pagination for notes" testPreviewsPagination
|
||||
it "chat pagination" testChatPagination
|
||||
it "stores files" testFiles
|
||||
it "deleting files does not interfere with other chat types" testOtherFiles
|
||||
|
||||
testNotes :: FilePath -> IO ()
|
||||
testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
createCCNoteFolder alice
|
||||
|
||||
alice ##> "/contacts"
|
||||
-- not a contact
|
||||
|
||||
alice /* "keep in mind"
|
||||
alice ##> "/tail"
|
||||
alice <# "* keep in mind"
|
||||
alice ##> "/chats"
|
||||
alice <# "* keep in mind"
|
||||
alice ##> "/? keep"
|
||||
alice <# "* keep in mind"
|
||||
|
||||
alice #$> ("/_read chat *1 from=1 to=100", id, "ok")
|
||||
alice ##> "/_unread chat *1 on"
|
||||
alice <## "ok"
|
||||
|
||||
alice ##> "/_delete item *1 1 internal"
|
||||
alice <## "message deleted"
|
||||
alice ##> "/tail"
|
||||
alice ##> "/chats"
|
||||
|
||||
alice /* "ahoy!"
|
||||
alice ##> "/_update item *1 1 text Greetings."
|
||||
alice ##> "/tail *"
|
||||
alice <# "* Greetings."
|
||||
|
||||
testUserNotes :: FilePath -> IO ()
|
||||
testUserNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
createCCNoteFolder alice
|
||||
|
||||
alice /* "keep in mind"
|
||||
alice ##> "/tail"
|
||||
alice <# "* keep in mind"
|
||||
|
||||
alice ##> "/create user secret"
|
||||
alice <## "user profile: secret"
|
||||
alice <## "use /p <display name> to change it"
|
||||
alice <## "(the updated profile will be sent to all your contacts)"
|
||||
|
||||
alice ##> "/tail"
|
||||
|
||||
alice ##> "/_delete item *1 1 internal"
|
||||
alice <## "chat db error: SENoteFolderNotFound {noteFolderId = 1}"
|
||||
|
||||
testPreviewsPagination :: FilePath -> IO ()
|
||||
testPreviewsPagination tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
createCCNoteFolder alice
|
||||
|
||||
tsS <- iso8601Show <$> getCurrentTime
|
||||
alice /* "first"
|
||||
tsM <- iso8601Show <$> getCurrentTime
|
||||
alice /* "last"
|
||||
tsE <- iso8601Show <$> getCurrentTime
|
||||
|
||||
-- there's only one folder that got updated after tsM and before tsE
|
||||
getChats_ alice "count=3" [("*", "last")]
|
||||
getChats_ alice ("after=" <> tsE <> " count=10") []
|
||||
getChats_ alice ("after=" <> tsS <> " count=10") [("*", "last")]
|
||||
getChats_ alice ("before=" <> tsM <> " count=10") []
|
||||
getChats_ alice ("before=" <> tsE <> " count=10") [("*", "last")]
|
||||
getChats_ alice ("before=" <> tsS <> " count=10") []
|
||||
|
||||
testChatPagination :: FilePath -> IO ()
|
||||
testChatPagination tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
createCCNoteFolder alice
|
||||
|
||||
alice /* "hello world"
|
||||
alice /* "memento mori"
|
||||
alice /* "knock-knock"
|
||||
alice /* "who's there?"
|
||||
|
||||
alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")])
|
||||
alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")])
|
||||
alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||
alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")])
|
||||
alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
||||
alice #$> ("/_get chat *1 before=3 count=10", chat, [(1, "hello world"), (1, "memento mori")])
|
||||
alice #$> ("/_get chat *1 before=3 count=2", chat, [(1, "hello world"), (1, "memento mori")])
|
||||
alice #$> ("/_get chat *1 before=4 count=2", chat, [(1, "memento mori"), (1, "knock-knock")])
|
||||
|
||||
alice #$> ("/_get chat *1 count=10 search=k-k", chat, [(1, "knock-knock")])
|
||||
|
||||
testFiles :: FilePath -> IO ()
|
||||
testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
-- setup
|
||||
createCCNoteFolder alice
|
||||
let files = "./tests/tmp/app_files"
|
||||
alice ##> ("/_files_folder " <> files)
|
||||
alice <## "ok"
|
||||
|
||||
-- ui-like upload
|
||||
let source = "./tests/fixtures/test.jpg"
|
||||
let stored = files </> "test.jpg"
|
||||
copyFile source stored
|
||||
alice ##> "/_create *1 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"hi myself\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
|
||||
alice <# "* hi myself"
|
||||
alice <# "* file 1 (test.jpg)"
|
||||
|
||||
alice ##> "/tail"
|
||||
alice <# "* hi myself"
|
||||
alice <# "* file 1 (test.jpg)"
|
||||
|
||||
alice ##> "/_get chat *1 count=100"
|
||||
r <- chatF <$> getTermLine alice
|
||||
r `shouldBe` [((1, "hi myself"), Just "test.jpg")]
|
||||
|
||||
alice ##> "/fs 1"
|
||||
alice <## "bad chat command: not supported for local files"
|
||||
|
||||
alice ##> "/fc 1"
|
||||
alice <## "chat db error: SELocalFileNoTransfer {fileId = 1}"
|
||||
|
||||
-- one more file
|
||||
let stored2 = files </> "another_test.jpg"
|
||||
copyFile source stored2
|
||||
alice ##> "/_create *1 json {\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
|
||||
alice <# "* file 2 (another_test.jpg)"
|
||||
|
||||
alice ##> "/_delete item *1 2 internal"
|
||||
alice <## "message deleted"
|
||||
doesFileExist stored2 `shouldReturn` False
|
||||
doesFileExist stored `shouldReturn` True
|
||||
|
||||
alice ##> "/clear *"
|
||||
alice ##> "/fs 1"
|
||||
alice <## "chat db error: SEChatItemNotFoundByFileId {fileId = 1}"
|
||||
alice ##> "/tail"
|
||||
doesFileExist stored `shouldReturn` False
|
||||
|
||||
testOtherFiles :: FilePath -> IO ()
|
||||
testOtherFiles =
|
||||
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
||||
connectUsers alice bob
|
||||
createCCNoteFolder bob
|
||||
bob ##> "/_files_folder ./tests/tmp/"
|
||||
bob <## "ok"
|
||||
alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}"
|
||||
alice <# "@bob voice message (00:10)"
|
||||
alice <# "/f @bob ./tests/fixtures/test.jpg"
|
||||
-- below is not shown in "sent" mode
|
||||
-- alice <## "use /fc 1 to cancel sending"
|
||||
bob <# "alice> voice message (00:10)"
|
||||
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
|
||||
-- below is not shown in "sent" mode
|
||||
-- bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
bob <## "started receiving file 1 (test.jpg) from alice"
|
||||
concurrently_
|
||||
(alice <## "completed sending file 1 (test.jpg) to bob")
|
||||
(bob <## "completed receiving file 1 (test.jpg) from alice")
|
||||
|
||||
bob /* "test"
|
||||
bob ##> "/tail *"
|
||||
bob <# "* test"
|
||||
bob ##> "/clear *"
|
||||
bob ##> "/tail *"
|
||||
bob ##> "/fs 1"
|
||||
bob <## "receiving file 1 (test.jpg) complete, path: test.jpg"
|
||||
doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True
|
||||
where
|
||||
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
|
||||
@@ -11,6 +11,7 @@ import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (unless, when)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isDigit)
|
||||
import Data.List (isPrefixOf, isSuffixOf)
|
||||
@@ -20,6 +21,7 @@ import qualified Data.Text as T
|
||||
import Database.SQLite.Simple (Only (..))
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store.NoteFolders (createNoteFolder)
|
||||
import Simplex.Chat.Store.Profiles (getUserContactProfiles)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
@@ -287,6 +289,11 @@ cc <##.. ls = do
|
||||
unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l)
|
||||
prefix `shouldBe` True
|
||||
|
||||
(/*) :: HasCallStack => TestCC -> String -> IO ()
|
||||
cc /* note = do
|
||||
cc `send` ("/* " <> note)
|
||||
(dropTime <$> getTermLine cc) `shouldReturn` ("* " <> note)
|
||||
|
||||
data ConsoleResponse
|
||||
= ConsoleString String
|
||||
| WithTime String
|
||||
@@ -462,6 +469,12 @@ withCCTransaction :: TestCC -> (DB.Connection -> IO a) -> IO a
|
||||
withCCTransaction cc action =
|
||||
withTransaction (chatStore $ chatController cc) $ \db -> action db
|
||||
|
||||
createCCNoteFolder :: TestCC -> IO ()
|
||||
createCCNoteFolder cc =
|
||||
withCCTransaction cc $ \db ->
|
||||
withCCUser cc $ \user ->
|
||||
runExceptT (createNoteFolder db user) >>= either (fail . show) pure
|
||||
|
||||
getProfilePictureByName :: TestCC -> String -> IO (Maybe String)
|
||||
getProfilePictureByName cc displayName =
|
||||
withTransaction (chatStore $ chatController cc) $ \db ->
|
||||
|
||||
@@ -16,11 +16,12 @@ import qualified Data.Aeson.TH as JQ
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import Data.ByteString.Internal (create, memcpy)
|
||||
import Data.ByteString.Internal (create)
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Word (Word8, Word32)
|
||||
import Foreign.C
|
||||
import Foreign.Marshal.Alloc (mallocBytes)
|
||||
import Foreign.Marshal.Utils (copyBytes)
|
||||
import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (peek)
|
||||
@@ -291,7 +292,7 @@ testFileCApi fileName tmp = do
|
||||
peek ptr' `shouldReturn` (0 :: Word8)
|
||||
sz :: Word32 <- peek (ptr' `plusPtr` 1)
|
||||
let sz' = fromIntegral sz
|
||||
contents <- create sz' $ \toPtr -> memcpy toPtr (ptr' `plusPtr` 5) sz'
|
||||
contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz'
|
||||
contents `shouldBe` src
|
||||
sz' `shouldBe` fromIntegral len
|
||||
|
||||
|
||||
Reference in New Issue
Block a user