Merge origin/master into sh/namespace

The names (simplex_name / RSLV) feature and master's badge feature both
extended the contact/group profile row layer. Resolution keeps both, with
simplex_name ordered last (chronological - it is the newer column):
- Profile/LocalProfile gain badge + simplex_name; simplex_name last in the
  data types, record builds, schema, and SQL row types/SELECTs/INSERTs
- SQL row types, SELECTs and INSERT/UPDATE lists carry both badge_* and
  simplex_name columns (simplex_name after badge)
- migration lists ordered by date (master 0601/0602 before names 0603+)
- SQLite chat_schema.sql regenerated; Postgres chat_schema.sql hand-merged

Verified: lib + test suite build; SchemaDump, Operators, Protocol and
direct/group profile round-trip tests pass.
This commit is contained in:
shum
2026-06-23 12:28:56 +00:00
191 changed files with 11177 additions and 1653 deletions
+142
View File
@@ -0,0 +1,142 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module BadgeTests (badgeTests) where
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime, nominalDay)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import qualified Data.Aeson as J
import qualified Simplex.Messaging.Crypto as C
import Simplex.Chat.Badges
import Simplex.Messaging.Crypto.BBS
import Test.Hspec
badgeTests :: Spec
badgeTests = do
it "full workflow: request, issue, verify credential, generate and verify proof" testFullWorkflow
it "should reject badge with tampered type" testTamperedType
it "should reject badge with tampered expiry" testTamperedExpiry
it "should reject badge with wrong server key" testWrongKey
it "should report a key index missing from configured keys" testUnknownKeyIdx
it "should compute badge status correctly" testExpiryCheck
it "should treat lifetime badges as always active" testLifetimeBadge
it "should accept unknown badge types" testUnknownBadgeType
it "credential serializes to a paste-able token and back" testCredentialSerialization
proofOf :: BadgeProof -> BBSProof
proofOf (BadgeProof _ _ p _) = p
proofInfo :: BadgeProof -> BadgeInfo
proofInfo (BadgeProof _ _ _ i) = i
testKeyIdx :: Int
testKeyIdx = 1
keysFor :: BBSPublicKey -> Map Int BBSPublicKey
keysFor = M.singleton testKeyIdx
testFullWorkflow :: IO ()
testFullWorkflow = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let req = BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = Just futureTime, badgeExtra = ""}}
Just vreq <- verifyPayment (BPRedeemCode "TEST") req
Right cred <- issueBadge testKeyIdx sk vreq
let BadgeCredential idx mk' _ _ = cred
idx `shouldBe` testKeyIdx
mk' `shouldBe` mk
verifyCredential pk cred >>= (`shouldBe` True)
Right badge <- generateBadgeProof pk cred (BBSPresHeader "nonce-1")
-- the proof inherits the credential's key index, so receivers find the right key
let BadgeProof {badgeKeyIdx} = badge
badgeKeyIdx `shouldBe` testKeyIdx
verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True)
Right badge2 <- generateBadgeProof pk cred (BBSPresHeader "nonce-2")
verifyBadge (keysFor pk) badge2 >>= (`shouldBe` Just True)
proofOf badge `shouldNotBe` proofOf badge2
testTamperedType :: IO ()
testTamperedType = do
(pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter (Just futureTime)
verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeType = BTLegend}) >>= (`shouldBe` Just False)
testTamperedExpiry :: IO ()
testTamperedExpiry = do
(pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter (Just futureTime)
verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeExpiry = Just pastTime}) >>= (`shouldBe` Just False)
testWrongKey :: IO ()
testWrongKey = do
(_, badge) <- issueBadgeProof BTSupporter (Just futureTime)
Right (pk2, _) <- bbsKeyGen
verifyBadge (keysFor pk2) badge >>= (`shouldBe` Just False)
testUnknownKeyIdx :: IO ()
testUnknownKeyIdx = do
(pk, badge) <- issueBadgeProof BTSupporter (Just futureTime)
-- a key index not in the configured keys cannot be verified at all (Nothing)
verifyBadge (M.singleton (testKeyIdx + 1) pk) badge >>= (`shouldBe` Nothing)
testExpiryCheck :: IO ()
testExpiryCheck = do
now <- getCurrentTime
let info expiry = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""}
futureInfo = info (Just futureTime)
mkBadgeStatus now (Just True) futureInfo `shouldBe` BSActive
mkBadgeStatus now (Just True) (info (Just (addUTCTime (-nominalDay) now))) `shouldBe` BSExpired
mkBadgeStatus now (Just True) (info (Just pastTime)) `shouldBe` BSExpiredOld
mkBadgeStatus now (Just False) futureInfo `shouldBe` BSFailed
mkBadgeStatus now Nothing futureInfo `shouldBe` BSUnknownKey
testLifetimeBadge :: IO ()
testLifetimeBadge = do
now <- getCurrentTime
(pk, badge) <- issueBadgeProof BTInvestor Nothing
verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True)
mkBadgeStatus now (Just True) (proofInfo badge) `shouldBe` BSActive
testUnknownBadgeType :: IO ()
testUnknownBadgeType = do
(pk, badge) <- issueBadgeProof (BTUnknown "future_type") (Just futureTime)
verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True)
testCredentialSerialization :: IO ()
testCredentialSerialization = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let mkCred expiry = do
Right cred <- issueBadge testKeyIdx sk (VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""}})
pure cred
dated <- mkCred (Just futureTime)
lifetime <- mkCred Nothing
J.eitherDecode (J.encode dated) `shouldBe` Right dated
J.eitherDecode (J.encode lifetime) `shouldBe` Right lifetime
-- a decoded credential still verifies against the issuing key
case J.eitherDecode (J.encode dated) of
Right cred -> verifyCredential pk cred >>= (`shouldBe` True)
Left e -> expectationFailure e
-- Helpers
futureTime :: UTCTime
futureTime = posixSecondsToUTCTime 4102444800 -- 2099-12-31
pastTime :: UTCTime
pastTime = posixSecondsToUTCTime 1577836800 -- 2020-01-01
issueBadgeProof :: BadgeType -> Maybe UTCTime -> IO (BBSPublicKey, BadgeProof)
issueBadgeProof bt expiry = do
Right (pk, sk) <- bbsKeyGen
drg <- C.newRandom
mk <- generateMasterKey drg
let vreq = VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = bt, badgeExpiry = expiry, badgeExtra = ""}}
Right cred <- issueBadge testKeyIdx sk vreq
Right badge <- generateBadgeProof pk cred (BBSPresHeader "test-nonce")
pure (pk, badge)
+1 -1
View File
@@ -33,7 +33,7 @@ withBroadcastBot opts test =
bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts
broadcastBotProfile :: Profile
broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Nothing, peerType = Just CPTBot, preferences = Nothing}
broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, simplexName = Nothing}
mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts
mkBotOpts ps publishers =
+4 -2
View File
@@ -27,8 +27,10 @@ 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.Protocol (memberSupportVoiceVersion)
import Simplex.Chat.Types (ChatPeerType (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Messaging.Version
import System.FilePath ((</>))
import Test.Hspec hiding (it)
@@ -96,7 +98,7 @@ directoryServiceTests = do
it "should update subscriber count periodically" testLinkCheckUpdatesCount
directoryProfile :: Profile
directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Nothing, peerType = Just CPTBot, preferences = Nothing}
directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, simplexName = Nothing}
mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts
mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder =
@@ -1492,7 +1494,7 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do
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
withNewTestChatCfg ps testCfg {chatVRange = (chatVRange testCfg) {maxVersion = prevVersion memberSupportVoiceVersion}} "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
bob #> "@'SimpleX Directory' /role 1"
+7 -3
View File
@@ -24,11 +24,12 @@ import Control.Monad.Reader
import Data.Functor (($>))
import Data.List (dropWhileEnd, find)
import Data.Maybe (isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime)
import Network.Socket
import Simplex.Chat
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg)
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), WebPreviewConfig (..), defaultSimpleNetCfg)
import Simplex.Chat.Core
import Simplex.Chat.Library.Commands
import Simplex.Chat.Options
@@ -153,6 +154,7 @@ testCoreOpts =
tbqSize = 16,
deviceName = Nothing,
chatRelay = False,
webPreviewConfig = Nothing,
highlyAvailable = False,
yesToUpMigrations = False,
migrationBackupPath = Nothing,
@@ -162,6 +164,9 @@ testCoreOpts =
relayTestOpts :: ChatOpts
relayTestOpts = testOpts {coreOptions = testCoreOpts {chatRelay = True}}
relayWebTestOpts :: Text -> FilePath -> Maybe FilePath -> ChatOpts
relayWebTestOpts webDomain webDir webCorsFile = testOpts {coreOptions = testCoreOpts {chatRelay = True, webPreviewConfig = Just WebPreviewConfig {webDomain, webJsonDir = webDir, webCorsFile, webUpdateInterval = 300, webPreviewItemCount = 50}}}
#if !defined(dbPostgres)
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
getTestOpts maintenance dbKey = testOpts {coreOptions = testCoreOpts {maintenance, dbOptions = (dbOptions testCoreOpts) {dbKey}}}
@@ -212,7 +217,7 @@ testCfg =
shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"],
testView = True,
tbqSize = 16,
channelSubscriberRole = GRMember, -- starting role is GRMember to test members sending messages
channelSubscriberRole = GRObserver,
confirmMigrations = MCYesUp
}
@@ -582,7 +587,6 @@ smpServerCfg =
allowSMPProxy = True,
serverClientConcurrency = 16,
namesConfig = Nothing,
namesResolverCall_ = Nothing,
information = Nothing,
startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp}
}
+289
View File
@@ -1,11 +1,14 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module ChatTests.ChatRelays where
import ChatClient
import ChatTests.DBUtils
import ChatTests.Groups (memberJoinChannel, memberJoinChannel', prepareChannel, prepareChannel', prepareChannel1Relay, setupRelay)
import ChatTests.Profiles (addTestBadge, issueTestBadge, testBadgeKeys)
import ChatTests.Utils
import Control.Concurrent (threadDelay)
import qualified Data.Aeson as J
@@ -14,10 +17,17 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import ProtocolTests (testGroupProfile)
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..))
import Simplex.Chat.Types (GroupProfile (..))
import Simplex.Chat.Controller (CorsOrigin (..))
import Simplex.Chat.Web (WebChannelPreview (..), WebMessage (..), extractOrigin, removeStaleFiles, writeCorsConfig)
import Simplex.Messaging.Crypto.BBS (bbsKeyGen)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Util (decodeJSON)
import qualified Data.Set as S
import System.Directory (createDirectoryIfMissing, doesFileExist, listDirectory)
import System.FilePath (takeExtension, (</>))
import Test.Hspec hiding (it)
chatRelayTests :: SpecWith TestParams
@@ -28,10 +38,57 @@ chatRelayTests = do
it "re-add soft-deleted relay by same name" testReAddRelaySameName
it "test chat relay" testChatRelayTest
it "relay profile updated in address" testRelayProfileUpdateInAddress
describe "relay capabilities" $ do
it "relay sends webDomain in capabilities" testRelayWebCapabilities
describe "web preview" $ do
it "render messages and members" testWebPreviewRender
it "incremental render adds new messages" testWebPreviewIncremental
it "edited and deleted messages" testWebPreviewEditedDeleted
it "reactions in rendered messages" testWebPreviewReactions
it "non-public group produces no file" testWebPreviewNonPublic
it "multiple channels produce multiple files" testWebPreviewMultipleChannels
it "channel deletion removes preview file" testWebPreviewChannelDeleted
it "removeStaleFiles preserves non-base64url files" testWebPreviewStaleCleanup
it "generate CORS config" testWebPreviewCors
it "extractOrigin strips path from URL" testExtractOrigin
describe "share channel card" $ do
it "share channel card in direct chat" testShareChannelDirect
it "share channel card in group" testShareChannelGroup
it "share channel card in channel" testShareChannelChannel
describe "channel badges" $ do
it "subscriber and owner see each other's badges forwarded by the relay" testChannelMemberBadges
-- A channel owner and a subscriber each hold a supporter badge; their member profiles only reach
-- each other forwarded by the relay. Both sides should still see the other's active badge.
testChannelMemberBadges :: HasCallStack => TestParams -> IO ()
testChannelMemberBadges ps = do
Right (pk, sk) <- bbsKeyGen
let cfg = testCfg {badgePublicKeys = testBadgeKeys pk}
withNewTestChatCfgOpts ps cfg testOpts "alice" aliceProfile $ \alice ->
withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChatCfgOpts ps cfg testOpts "cath" cathProfile $ \cath -> do
addTestBadge alice =<< issueTestBadge sk Nothing
addTestBadge cath =<< issueTestBadge sk Nothing
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
-- a channel message lets the relay-forwarded member profiles settle on both sides
alice #> "#team hi"
bob <# "#team> hi"
cath <# "#team> hi [>>]"
threadDelay 1000000
-- owner and subscriber are connected only via the relay, so /i shows the badge then "member not connected" for both
alice ##> "/i #team cath"
alice <## "group ID: 1"
alice <##. "member ID: "
alice <## "supporter badge - active"
alice <## "no expiry"
alice <## "member not connected"
cath ##> "/i #team alice"
cath <## "group ID: 1"
cath <##. "member ID: "
cath <## "supporter badge - active"
cath <## "no expiry"
cath <## "member not connected"
testGetSetChatRelays :: HasCallStack => TestParams -> IO ()
testGetSetChatRelays ps =
@@ -325,6 +382,238 @@ testShareChannelChannel ps =
getTermLine2 :: TestCC -> IO (String, String)
getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c
testRelayWebCapabilities :: HasCallStack => TestParams -> IO ()
testRelayWebCapabilities ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" (tmpPath ps </> "web_cap") Nothing) "bob" bobProfile $ \relay -> do
rName <- userName relay
relay ##> "/ad"
(relaySLink, _cLink) <- getContactLinks relay True
alice ##> ("/relays name=" <> rName <> " " <> relaySLink)
alice <## "ok"
alice ##> "/public group relays=1 #news"
alice <## "group #news is created"
alice <## "wait for selected relay(s) to join, then you can invite members via group link"
concurrentlyN_
[ do
alice <## "#news: group link relays updated, current relays:"
alice <### [EndsWith ": active, web: relay.example.com"]
alice <## "group link:"
_ <- getTermLine alice
pure (),
relay <## "#news: you joined the group as relay"
]
-- Helper: set up relay with web config + channel
withWebChannel :: TestParams -> String -> (TestCC -> TestCC -> FilePath -> IO ()) -> IO ()
withWebChannel ps gName test = do
let webDir = tmpPath ps </> "web_" <> gName
corsFile = tmpPath ps </> "cors_" <> gName <> ".conf"
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir (Just corsFile)) "bob" bobProfile $ \relay -> do
_ <- setupRelay alice relay
createChannelWithRelayWeb gName alice relay
test alice relay webDir
createChannelWithRelayWeb :: HasCallStack => String -> TestCC -> TestCC -> IO ()
createChannelWithRelayWeb gName owner relay = do
owner ##> ("/public group relays=1 #" <> gName)
owner <## ("group #" <> gName <> " is created")
owner <## "wait for selected relay(s) to join, then you can invite members via group link"
concurrentlyN_
[ do
owner <## ("#" <> gName <> ": group link relays updated, current relays:")
owner <### [EndsWith ": active, web: relay.example.com"]
owner <## "group link:"
_ <- getTermLine owner
pure (),
relay <## ("#" <> gName <> ": you joined the group as relay")
]
-- Poll for a JSON preview file written by the worker that satisfies predicate, with timeout
waitPreviewWith :: HasCallStack => FilePath -> (WebChannelPreview -> Bool) -> IO WebChannelPreview
waitPreviewWith webDir check = go 50
where
go :: Int -> IO WebChannelPreview
go 0 = error "waitPreview: timed out waiting for matching JSON file"
go n = do
files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir
case files of
[f] -> do
jsonBytes <- LB.readFile (webDir </> f)
case J.eitherDecode jsonBytes of
Right p | check p -> pure p
_ -> threadDelay 100000 >> go (n - 1)
_ -> threadDelay 100000 >> go (n - 1)
waitPreview :: HasCallStack => FilePath -> IO WebChannelPreview
waitPreview webDir = waitPreviewWith webDir (const True)
testWebPreviewRender :: HasCallStack => TestParams -> IO ()
testWebPreviewRender ps =
withWebChannel ps "news" $ \alice relay webDir -> do
alice #> "#news hello from the channel"
relay <# "#news> hello from the channel"
alice #> "#news second message"
relay <# "#news> second message"
wPreview <- waitPreviewWith webDir (\p -> length (messages p) >= 2)
let GroupProfile {displayName = chName} = channel wPreview
chName `shouldBe` "news"
length (messages wPreview) `shouldBe` 2
content (messages wPreview !! 0) `shouldBe` MCText "hello from the channel"
content (messages wPreview !! 1) `shouldBe` MCText "second message"
length (members wPreview) `shouldSatisfy` (>= 1)
all (\m -> ts m > read "2020-01-01 00:00:00 UTC") (messages wPreview) `shouldBe` True
jsonFiles <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir
length jsonFiles `shouldBe` 1
testWebPreviewIncremental :: HasCallStack => TestParams -> IO ()
testWebPreviewIncremental ps =
withWebChannel ps "inc" $ \alice relay webDir -> do
alice #> "#inc first"
relay <# "#inc> first"
p1 <- waitPreviewWith webDir (\p -> length (messages p) >= 1)
length (messages p1) `shouldBe` 1
content (messages p1 !! 0) `shouldBe` MCText "first"
alice #> "#inc second"
relay <# "#inc> second"
alice #> "#inc third"
relay <# "#inc> third"
p2 <- waitPreviewWith webDir (\p -> length (messages p) >= 3)
length (messages p2) `shouldBe` 3
content (messages p2 !! 0) `shouldBe` MCText "first"
content (messages p2 !! 1) `shouldBe` MCText "second"
content (messages p2 !! 2) `shouldBe` MCText "third"
testWebPreviewEditedDeleted :: HasCallStack => TestParams -> IO ()
testWebPreviewEditedDeleted ps =
withWebChannel ps "ed" $ \alice relay webDir -> do
alice #> "#ed msg one"
relay <# "#ed> msg one"
alice #> "#ed msg two"
relay <# "#ed> msg two"
msgId2 <- lastItemId alice
alice #> "#ed msg three"
relay <# "#ed> msg three"
msgId3 <- lastItemId alice
alice ##> ("/_update item #1 " <> msgId2 <> " text msg two edited")
alice <# "#ed [edited] msg two edited"
relay <# "#ed> [edited] msg two edited"
alice #$> ("/_delete item #1 " <> msgId3 <> " broadcast", id, "message marked deleted")
relay <# "#ed> [marked deleted] msg three"
p <- waitPreviewWith webDir (\p -> length (messages p) == 2 && any edited (messages p))
length (messages p) `shouldBe` 2
content (messages p !! 0) `shouldBe` MCText "msg one"
content (messages p !! 1) `shouldBe` MCText "msg two edited"
edited (messages p !! 0) `shouldBe` False
edited (messages p !! 1) `shouldBe` True
testWebPreviewReactions :: HasCallStack => TestParams -> IO ()
testWebPreviewReactions ps =
withWebChannel ps "react" $ \alice relay webDir -> do
alice #> "#react hello"
relay <# "#react> hello"
alice ##> "+1 #react hello"
alice <## "added 👍"
relay <# "#react alice> > hello"
relay <## " + 👍"
p <- waitPreviewWith webDir (\p -> not (null (messages p)) && not (null (reactions (head (messages p)))))
length (messages p) `shouldBe` 1
length (reactions (messages p !! 0)) `shouldSatisfy` (>= 1)
testWebPreviewNonPublic :: HasCallStack => TestParams -> IO ()
testWebPreviewNonPublic ps = do
let webDir = tmpPath ps </> "web_nonpub"
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir Nothing) "bob" bobProfile $ \relay -> do
_ <- setupRelay alice relay
alice ##> "/g private"
alice <## "group #private is created"
alice <## "to add members use /a private <name> or /create link #private"
alice #> "#private hello"
threadDelay 2000000
files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir
length files `shouldBe` 0
testWebPreviewMultipleChannels :: HasCallStack => TestParams -> IO ()
testWebPreviewMultipleChannels ps = do
let webDir = tmpPath ps </> "web_multi"
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir Nothing) "bob" bobProfile $ \relay -> do
_ <- setupRelay alice relay
createChannelWithRelayWeb "ch1" alice relay
createChannelWithRelayWeb "ch2" alice relay
alice #> "#ch1 msg in ch1"
relay <# "#ch1> msg in ch1"
alice #> "#ch2 msg in ch2"
relay <# "#ch2> msg in ch2"
threadDelay 2000000
files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir
length files `shouldBe` 2
testWebPreviewChannelDeleted :: HasCallStack => TestParams -> IO ()
testWebPreviewChannelDeleted ps =
withWebChannel ps "del" $ \alice relay webDir -> do
alice #> "#del hello"
relay <# "#del> hello"
_ <- waitPreviewWith webDir (\p -> not (null (messages p)))
jsonFiles <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir
length jsonFiles `shouldBe` 1
let previewFile = webDir </> head jsonFiles
alice ##> "/d #del"
alice <## "#del: you deleted the group (signed)"
relay <## "#del: alice deleted the group (signed)"
relay <## "use /d #del to delete the local copy of the group"
waitFileDeleted previewFile 50
testWebPreviewStaleCleanup :: HasCallStack => TestParams -> IO ()
testWebPreviewStaleCleanup ps = do
let webDir = tmpPath ps </> "web_stale_unit"
activeFile = "abc123.json"
staleFile = "AAAA_stale.json"
safeFile = "my.config.json"
createDirectoryIfMissing True webDir
writeFile (webDir </> activeFile) "{}"
writeFile (webDir </> staleFile) "{}"
writeFile (webDir </> safeFile) "{}"
removeStaleFiles webDir (S.singleton activeFile)
doesFileExist (webDir </> staleFile) `shouldReturn` False
doesFileExist (webDir </> safeFile) `shouldReturn` True
doesFileExist (webDir </> activeFile) `shouldReturn` True
waitFileDeleted :: HasCallStack => FilePath -> Int -> IO ()
waitFileDeleted _ 0 = error "waitFileDeleted: timed out"
waitFileDeleted path n =
doesFileExist path >>= \case
False -> pure ()
True -> threadDelay 100000 >> waitFileDeleted path (n - 1)
testWebPreviewCors :: HasCallStack => TestParams -> IO ()
testWebPreviewCors ps = do
let corsFile = tmpPath ps </> "simplex-cors.conf"
entries =
[ ("abc123.json", CorsAny),
("def456.json", CorsOrigins ["https://owner-site.com"]),
("ghi789.json", CorsOrigins [])
]
writeCorsConfig entries corsFile
corsContent <- readFile corsFile
corsContent `shouldContain` "/channel/abc123.json \"*\""
corsContent `shouldContain` "/channel/def456.json \"https://owner-site.com\""
corsContent `shouldContain` "# ghi789.json (no origin configured)"
corsContent `shouldContain` "Access-Control-Allow-Origin"
corsContent `shouldContain` "Access-Control-Allow-Methods"
testExtractOrigin :: HasCallStack => TestParams -> IO ()
testExtractOrigin _ps = do
extractOrigin "https://owner.example.com/channel.html" `shouldBe` Just "https://owner.example.com"
extractOrigin "https://owner.example.com/path/to/page?q=1#frag" `shouldBe` Just "https://owner.example.com"
extractOrigin "https://owner.example.com:8443/page" `shouldBe` Just "https://owner.example.com:8443"
extractOrigin "https://owner.example.com" `shouldBe` Just "https://owner.example.com"
extractOrigin "http://localhost:3000/preview" `shouldBe` Just "http://localhost:3000"
extractOrigin "ftp://example.com/file" `shouldBe` Nothing
extractOrigin "not-a-url" `shouldBe` Nothing
-- Create a public group with relay=1, wait for relay to join
createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO ()
createChannelWithRelay gName owner relay = do
+20
View File
@@ -122,6 +122,7 @@ chatDirectTests = do
it "create user with same servers" testCreateUserSameServers
it "delete user" testDeleteUser
it "delete user with chat tags" testDeleteUserChatTags
it "rejects raw chat TTL updates for another user's chat" testRejectCrossUserChatTTL
it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL
it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration
it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser
@@ -2096,6 +2097,25 @@ testDeleteUserChatTags =
alice ##> "/users"
alice <## "alisa (active)"
testRejectCrossUserChatTTL :: HasCallStack => TestParams -> IO ()
testRejectCrossUserChatTTL =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice #$> ("/_ttl 1 @2 2", id, "ok")
alice #$> ("/ttl @bob", id, "old messages are set to be deleted after: 2 second(s)")
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice ##> "/_ttl 2 @2 9"
alice <##. "chat db error:"
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
alice #$> ("/ttl @bob", id, "old messages are set to be deleted after: 2 second(s)")
testUsersDifferentCIExpirationTTL :: HasCallStack => TestParams -> IO ()
testUsersDifferentCIExpirationTTL ps = do
withNewTestChat ps "bob" bobProfile $ \bob -> do
+741 -87
View File
File diff suppressed because it is too large Load Diff
+233 -8
View File
@@ -1,4 +1,5 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -18,11 +19,18 @@ import Control.Monad.Except
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime, nominalDay)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import qualified Data.Map.Strict as M
import Simplex.Chat.Badges (BadgeCredential, BadgeInfo (..), BadgePurchase (..), BadgeRequest (..), BadgeType (..), generateMasterKey, issueBadge, verifyPayment)
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatHooks (..), defaultChatHooks, mkStoreCxt)
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..))
import Simplex.Chat.Protocol (currentChatVersion)
import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen)
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Env.SQLite
@@ -40,6 +48,13 @@ chatProfileTests = do
it "update user profile and notify contacts" testUpdateProfile
it "update user profile with image" testUpdateProfileImage
it "use multiword profile names" testMultiWordProfileNames
it "present supporter badge to contacts" testUserBadgeBroadcast
it "supporter badge sent to contact connecting after attach" testUserBadgeOnConnect
it "supporter badge sent to member joining via group link" testUserBadgeGroupLink
it "expired supporter badge shows as expired" testUserBadgeExpired
it "long-expired supporter badge is not presented" testUserBadgeExpiredOld
it "incognito connection does not carry supporter badge" testUserBadgeIncognito
it "supporter badge sent to contact connecting via address" testUserBadgeContactAddress
describe "user contact link" $ do
it "create and connect via contact link" testUserContactLink
it "retry connecting via contact link" testRetryConnectingViaContactLink
@@ -188,6 +203,210 @@ testUpdateProfile =
bob <## "use @cat <message> to send messages"
]
-- the test issuer key under index 1 in the test config
testBadgeKeys :: BBSPublicKey -> M.Map Int BBSPublicKey
testBadgeKeys = M.singleton 1
-- issue a supporter badge credential with the given expiry (test issuer)
issueTestBadge :: BBSSecretKey -> Maybe UTCTime -> IO BadgeCredential
issueTestBadge sk badgeExpiry = do
drg <- C.newRandom
mk <- generateMasterKey drg
let info = BadgeInfo {badgeType = BTSupporter, badgeExpiry, badgeExtra = ""}
Just vreq <- verifyPayment (BPRedeemCode "TEST") BadgeRequest {masterKey = mk, badgeInfo = info}
Right cred <- issueBadge 1 sk vreq
pure cred
-- the same single-line JSON `simplex-chat badge sign` prints, pasted into the app
addTestBadge :: HasCallStack => TestCC -> BadgeCredential -> IO ()
addTestBadge cc cred = do
cc ##> ("/badge add " <> T.unpack (encodeJSON cred))
cc <## "ok"
testUserBadgeBroadcast :: HasCallStack => TestParams -> IO ()
testUserBadgeBroadcast ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
connectUsers alice bob
addTestBadge alice =<< issueTestBadge sk Nothing
-- own badge is shown (add succeeded)
alice ##> "/p"
alice <## "user profile: alice (Alice, * supporter)"
alice <## "use /p <name> [<bio>] to change it"
-- the badge XInfo is delivered in order before this message, so the contact has stored it
alice #> "@bob hi"
bob <# "alice *> hi"
testUserBadgeOnConnect :: HasCallStack => TestParams -> IO ()
testUserBadgeOnConnect ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
addTestBadge alice =<< issueTestBadge sk Nothing
-- a contact connecting after the badge is attached receives it in the connection handshake
alice ##> "/c"
inv <- getInvitation alice
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
concurrently_
(bob <## "alice (Alice, * supporter): contact is connected")
(alice <## "bob (Bob): contact is connected")
bob ##> "/i alice"
bob <## "contact ID: 2"
bob <## "supporter badge - active"
bob <## "no expiry"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
bob <## currentChatVRangeInfo
testUserBadgeGroupLink :: HasCallStack => TestParams -> IO ()
testUserBadgeGroupLink ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
addTestBadge alice =<< issueTestBadge sk Nothing
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" GRMember True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
]
-- the host's profile (x.grp.link.mem) is sent over the same connection as group messages,
-- so receiving a message guarantees the badge arrived
alice #> "#team hello"
bob <# "#team alice> hello"
-- no prior contact: the host's badge arrives via the group link handshake
bob ##> "/i #team alice"
bob <## "group ID: 1"
bob <##. "member ID: "
bob <## "supporter badge - active"
bob <## "no expiry"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "connection not verified, use /code command to see security code"
bob <## currentChatVRangeInfo
testUserBadgeContactAddress :: HasCallStack => TestParams -> IO ()
testUserBadgeContactAddress ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
addTestBadge alice =<< issueTestBadge sk Nothing
alice ##> "/ad"
(shortLink, cLink) <- getContactLinks alice True
-- the address link data carries the badge proof; the connect plan returns it verified, without crypto
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "contact address: ok to connect"
sLinkData <- getTermLine bob
sLinkData `shouldContain` "\"proof\":"
sLinkData `shouldContain` "\"localBadge\":{\"badge\":{\"badgeType\":\"supporter\""
sLinkData `shouldContain` "\"status\":\"active\""
bob ##> ("/c " <> cLink)
alice <#? bob
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
(bob <## "alice (Alice, * supporter): contact is connected")
(alice <## "bob (Bob): contact is connected")
bob ##> "/i alice"
bob <## "contact ID: 2"
bob <## "supporter badge - active"
bob <## "no expiry"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
bob <## currentChatVRangeInfo
testUserBadgeExpired :: HasCallStack => TestParams -> IO ()
testUserBadgeExpired ps = do
Right (pk, sk) <- bbsKeyGen
-- expired recently (within 31 days), so the badge is still presented and shown as expired
expiry <- addUTCTime (-2 * nominalDay) <$> getCurrentTime
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk expiry) ps
where
test sk expiry alice bob = do
addTestBadge alice =<< issueTestBadge sk (Just expiry)
-- expired badge: no star
alice ##> "/p"
alice <## "user profile: alice (Alice)"
alice <## "use /p <name> [<bio>] to change it"
connectUsers alice bob
bob ##> "/i alice"
bob <## "contact ID: 2"
bob <## "supporter badge - expired"
bob <## ("expires " <> formatTime defaultTimeLocale "%Y-%m-%d" expiry)
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
bob <## currentChatVRangeInfo
testUserBadgeExpiredOld :: HasCallStack => TestParams -> IO ()
testUserBadgeExpiredOld ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
addTestBadge alice =<< issueTestBadge sk (Just pastDate)
-- a badge that expired over a month ago is not presented to contacts at all
connectUsers alice bob
bob ##> "/i alice"
bob <## "contact ID: 2"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
bob <## currentChatVRangeInfo
pastDate = posixSecondsToUTCTime 1577836800 -- 2020-01-01
testUserBadgeIncognito :: HasCallStack => TestParams -> IO ()
testUserBadgeIncognito ps = do
Right (pk, sk) <- bbsKeyGen
testChatCfg2 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile (test sk) ps
where
test sk alice bob = do
addTestBadge alice =<< issueTestBadge sk Nothing
-- an incognito identity must not carry the badge
bob ##> "/connect"
inv <- getInvitation bob
alice ##> ("/connect incognito " <> inv)
alice <## "confirmation sent!"
aliceIncognito <- getTermLine alice
concurrentlyN_
[ bob <## (aliceIncognito <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob to print out this incognito profile again"
]
bob ##> ("/i " <> aliceIncognito)
bob <## "contact ID: 2"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
bob <## currentChatVRangeInfo
testUpdateProfileImage :: HasCallStack => TestParams -> IO ()
testUpdateProfileImage =
testChat2 aliceProfile bobProfile $
@@ -282,7 +501,7 @@ testMultiWordProfileNames =
aliceProfile' = baseProfile {displayName = "Alice Jones"}
bobProfile' = baseProfile {displayName = "Bob James"}
cathProfile' = baseProfile {displayName = "Cath Johnson"}
baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Nothing, peerType = Nothing, preferences = defaultPrefs}
baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, simplexName = Nothing}
testUserContactLink :: HasCallStack => TestParams -> IO ()
testUserContactLink =
@@ -1190,13 +1409,13 @@ testPlanAddressContactViaAddress =
Left _ -> error "error parsing contact link"
Right cReq -> do
let profile = aliceProfile {contactLink = Just cReq}
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
bob ##> "/delete @alice"
bob <## "alice: contact is deleted"
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
bob ##> ("/_connect plan 1 " <> cLink)
@@ -1211,7 +1430,7 @@ testPlanAddressContactViaAddress =
alice ##> "/delete @bob"
alice <## "bob: contact is deleted"
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
-- GUI api
@@ -1252,13 +1471,13 @@ testPlanAddressContactViaShortAddress =
Left _ -> error "error parsing contact link"
Right shortLink -> do
let profile = aliceProfile {contactLink = Just shortLink}
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
bob ##> "/delete @alice"
bob <## "alice: contact is deleted"
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
bob ##> ("/_connect plan 1 " <> sLink)
@@ -1273,7 +1492,7 @@ testPlanAddressContactViaShortAddress =
alice ##> "/delete @bob"
alice <## "bob: contact is deleted"
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile
void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> let TestCC {chatController = ChatController {config}} = bob in runExceptT $ createContact db (mkStoreCxt config) user profile
bob @@@ [("@alice", "")]
-- GUI api
@@ -2687,6 +2906,12 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
bob <## "bad chat command: feature not allowed SimpleX links"
bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]")
bob <## "bad chat command: feature not allowed SimpleX links"
-- a link split with a space or a newline is still blocked
let (lnk1, lnk2) = splitAt 12 inv
bob ##> ("#team \"" <> lnk1 <> " " <> lnk2 <> "\"")
bob <## "bad chat command: feature not allowed SimpleX links"
bob ##> ("#team \"" <> lnk1 <> "\\n" <> lnk2 <> "\"")
bob <## "bad chat command: feature not allowed SimpleX links"
(alice </)
(cath </)
bob `send` ("@alice \"" <> inv <> "\\ntest\"")
+1 -1
View File
@@ -88,7 +88,7 @@ serviceProfile :: Profile
serviceProfile = mkProfile "service_user" "Service user" Nothing
mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, simplexName = Nothing, peerType = Nothing, preferences = defaultPrefs}
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, simplexName = Nothing}
it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation))
it name test =
+19
View File
@@ -25,6 +25,7 @@ markdownTests = do
textColor
textWithUri
textWithHyperlink
obfuscatedSimplexLinks
textWithEmail
textWithPhone
textWithMentions
@@ -284,6 +285,24 @@ textWithHyperlink = describe "text with HyperLink without link text" do
"[click here](example.com)" <==> "[click here](example.com)"
"[click here](https://example.com )" <==> "[click here](https://example.com )"
obfuscatedSimplexLinks :: Spec
obfuscatedSimplexLinks = describe "SimpleX links obfuscated with whitespace" do
let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw"
inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
let spaced s = T.replace "://" ":// " s -- insert a space right after the scheme
it "detects links split with spaces or newlines" do
hasObfuscatedSimplexLink addr `shouldBe` True
hasObfuscatedSimplexLink (spaced addr) `shouldBe` True
hasObfuscatedSimplexLink (T.intercalate "\n" $ T.chunksOf 8 addr) `shouldBe` True
hasObfuscatedSimplexLink ("connect with me: " <> spaced addr) `shouldBe` True
hasObfuscatedSimplexLink (T.intercalate " " $ T.chunksOf 8 $ "https://simplex.chat" <> inv) `shouldBe` True
it "detects a split link followed by other text" do
hasObfuscatedSimplexLink (spaced addr <> "\nplease connect") `shouldBe` True
it "ignores text without a SimpleX link" do
hasObfuscatedSimplexLink "" `shouldBe` False
hasObfuscatedSimplexLink "hello there, this is a normal message" `shouldBe` False
hasObfuscatedSimplexLink "see https://example.com/page?ref=123 for details" `shouldBe` False
email :: Text -> Markdown
email = Markdown $ Just Email
+52 -2
View File
@@ -12,14 +12,31 @@ import qualified Data.ByteString as B
import Data.ByteString.Internal (c2w)
import Data.Either (partitionEithers)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.String (IsString (..))
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
import Simplex.Chat.Delivery
( DeliveryJobScope (DJSGroup, jobSpec),
DeliveryJobSpec (DJDeliveryJob, includePending),
MessageDeliveryTask (MessageDeliveryTask, brokerTs, fwdSender, jobScope, senderGMId, taskId, verifiedMsg),
deliveryTaskId,
)
import Simplex.Chat.Messages.Batch
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..))
import Simplex.Chat.Messages (SndMessage (..))
import Simplex.Chat.Protocol (maxEncodedMsgLength)
import Simplex.Chat.Types (SharedMsgId (..))
import Simplex.Chat.Protocol
( ChatMessage (ChatMessage),
ChatMsgEvent (XMsgNew),
FwdSender (FwdChannel),
GrpMsgForward (GrpMsgForward),
MsgContent (MCText),
VerifiedMsg (VMUnsigned),
maxEncodedMsgLength,
mcSimple,
)
import Simplex.Chat.Types (SharedMsgId (..), chatInitialVRange)
import Simplex.Messaging.Encoding (Large (..), smpEncodeList)
import Test.Hspec
@@ -28,6 +45,8 @@ batchingTests = describe "message batching tests" $ do
testBatchingCorrectness
testBinaryBatchingCorrectness
it "image x.msg.new and x.msg.file.descr should fit into single batch" testImageFitsSingleBatch
it "does not create a relay delivery body when every task is oversized" testRelayBatchAllLarge
it "classifies a task that fits raw but not as a framed singleton as large" testRelayBatchSingletonOverflow
instance IsString SndMessage where
fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = s', signedMsg_ = Nothing}
@@ -131,6 +150,37 @@ testImageFitsSingleBatch = do
runBatcherTest' BMJson maxEncodedMsgLength [msg xMsgNewStr, msg descrStr] [] [batched]
testRelayBatchAllLarge :: IO ()
testRelayBatchAllLarge = do
let task1 = deliveryTask 1 "one"
task2 = deliveryTask 2 "two"
(body_, accepted, large) = batchDeliveryTasks1 chatInitialVRange 1 (task1 :| [task2])
body_ `shouldBe` Nothing
map deliveryTaskId accepted `shouldBe` []
map deliveryTaskId large `shouldBe` [1, 2]
deliveryTask :: Int64 -> T.Text -> MessageDeliveryTask
deliveryTask taskId text =
MessageDeliveryTask
{ taskId,
jobScope = DJSGroup {jobSpec = DJDeliveryJob {includePending = False}},
senderGMId = 1,
fwdSender = FwdChannel,
brokerTs = systemToUTCTime $ MkSystemTime 0 0,
verifiedMsg =
VMUnsigned
(ChatMessage chatInitialVRange Nothing $ XMsgNew $ mcSimple $ MCText text)
}
testRelayBatchSingletonOverflow :: IO ()
testRelayBatchSingletonOverflow = do
let task = deliveryTask 1 "overflow"
elemLen = B.length $ encodeFwdElement (GrpMsgForward (fwdSender task) (brokerTs task)) (verifiedMsg task)
(body_, accepted, large) = batchDeliveryTasks1 chatInitialVRange (elemLen + 2) (task :| [])
body_ `shouldBe` Nothing
map deliveryTaskId accepted `shouldBe` []
map deliveryTaskId large `shouldBe` [1]
runBatcherTest :: BatchMode -> Int -> [SndMessage] -> [ChatError] -> [ByteString] -> Spec
runBatcherTest mode maxLen msgs expectedErrors expectedBatches =
it
+23
View File
@@ -33,8 +33,10 @@ import Foreign.Storable (peek)
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
import JSONFixtures
import Simplex.Chat
import Simplex.Chat.Badges (BadgeInfo (..), BadgeRequest (..), BadgeType (..), generateMasterKey, verifyCredential)
import Simplex.Chat.Controller (ChatController (..), ChatDatabase (..))
import Simplex.Chat.Mobile hiding (error)
import Simplex.Chat.Mobile.Badges hiding (error)
import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Mobile.WebRTC
@@ -82,6 +84,8 @@ mobileTests = do
describe "Parsers" $ do
it "should parse server address" testChatParseServer
it "should parse and sanitize URI" testChatParseUri
describe "Badges" $ do
it "should generate key and issue badge via C API, verify credential" testBadgeKeygenIssueCApi
noActiveUser :: LB.ByteString
noActiveUser =
@@ -310,6 +314,25 @@ testChatParseUri :: TestParams -> IO ()
testChatParseUri _ = do
pure ()
-- Generate a server keypair and issue a badge credential via the C FFI,
-- constructing the request from the typed records, then verify the issued
-- credential's BBS signature on the Haskell side.
testBadgeKeygenIssueCApi :: TestParams -> IO ()
testBadgeKeygenIssueCApi _ = do
g <- C.newRandom
IssuerKeyPair {publicKey, secretKey} <- ffiResult =<< (peekCString =<< cChatBadgeKeygen)
mk <- generateMasterKey g
let req = BadgeIssueReq {badgeKeyIdx = 1, secretKey, request = BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = Nothing, badgeExtra = ""}}}
cred <- ffiResult =<< (peekCString =<< cChatBadgeIssue =<< newCString (LB.unpack (J.encode req)))
verifyCredential publicKey cred `shouldReturn` True
-- Decode an FFI `BadgeResult` envelope, returning the result or failing on error.
ffiResult :: FromJSON r => String -> IO r
ffiResult s = case J.eitherDecode (LB.pack s) of
Right (BadgeResult r) -> pure r
Right (BadgeError e) -> error $ "badge FFI error: " <> show e
Left e -> error $ "badge FFI decode failed: " <> e <> " in " <> s
jDecode :: FromJSON a => String -> IO (Maybe a)
jDecode = pure . J.decode . LB.pack
+34 -13
View File
@@ -9,7 +9,7 @@ module ProtocolTests where
import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
import Simplex.Chat.Library.Internal (redactedMemberProfile, userProfileInGroup')
import Simplex.Chat.Library.Internal (decodeLinkUserData, encodeShortLinkData, redactedMemberProfile, userProfileInGroup')
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -26,6 +26,7 @@ protocolTests :: Spec
protocolTests = do
decodeChatMessageTest
outgoingProfileSimplexNameTest
shortLinkDataTests
srv :: SMPServer
srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251")
@@ -107,7 +108,7 @@ testGroupPreferences :: Maybe GroupPreferences
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing}
testProfile :: Profile
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, simplexName = Nothing, preferences = testChatPreferences}
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, simplexName = Nothing}
testGroupProfile :: GroupProfile
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, simplexName = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing}
@@ -118,6 +119,25 @@ testSimplexName = SimplexNameInfo NTContact (SimplexNameDomain TLDSimplex "alice
testGroupSimplexName :: SimplexNameInfo
testGroupSimplexName = SimplexNameInfo NTPublicGroup (SimplexNameDomain TLDSimplex "team" [])
shortLinkDataTests :: Spec
shortLinkDataTests = describe "Short link data encoding/decoding" $ do
it "decodes compressed short-link user data below the decompressed size limit" $ do
let value = replicate 11000 'a'
decodeLinkUserData (linkData value) `shouldReturn` Just value
it "rejects compressed short-link user data above the decompressed size limit" $ do
let value = replicate (maxDecompressedMsgLength + 1) 'a'
decodeLinkUserData (linkData value) `shouldReturn` (Nothing :: Maybe String)
where
linkData value =
ContactLinkData
supportedSMPAgentVRange
UserContactData
{ direct = True,
owners = [],
relays = [],
userData = encodeShortLinkData (value :: String)
}
decodeChatMessageTest :: Spec
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
it "x.msg.new simple text" $
@@ -142,7 +162,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello")))
it "x.msg.new chat message with chat version range" $
"{\"v\":\"1-17\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
"{\"v\":\"1-19\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello")))
it "x.msg.new quote" $
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
@@ -227,10 +247,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XInfo testProfile
it "x.info with empty full name" $
"{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Nothing, peerType = Nothing, preferences = testChatPreferences}
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, simplexName = Nothing}
it "x.info with simplexName" $
"{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"simplexName\":{\"nameType\":\"contact\",\"nameDomain\":{\"nameTLD\":\"simplex\",\"domain\":\"alice\",\"subDomain\":[]}},\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Just testSimplexName, peerType = Nothing, preferences = testChatPreferences}
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, simplexName = Just testSimplexName}
it "x.contact with xContactId" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing
@@ -259,13 +279,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing
it "x.grp.mem.new with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing
it "x.grp.mem.intro" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing
it "x.grp.mem.intro with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing
it "x.grp.mem.intro with member restrictions" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
@@ -280,7 +300,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
@@ -293,7 +313,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XGrpMemConAll (MemberId "\1\2\3\4")
it "x.grp.mem.del" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpMemDel (MemberId "\1\2\3\4") False
#==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing
it "x.grp.leave" $
"{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}"
==# XGrpLeave
@@ -340,10 +360,11 @@ testUser sn =
shortDescr = Nothing,
image = Nothing,
contactLink = Nothing,
simplexName = sn,
preferences = Nothing,
peerType = Nothing,
localAlias = ""
localBadge = Nothing,
localAlias = "",
simplexName = sn
},
fullPreferences = fullPreferences' Nothing,
activeUser = True,
@@ -368,13 +389,13 @@ outgoingProfileSimplexNameTest = describe "outgoing Profile carries User.profile
let Profile {simplexName = sn} = userProfileDirect (testUser Nothing) Nothing Nothing True
sn `shouldBe` Nothing
it "userProfileDirect with incognito profile suppresses simplexName" $ do
let incognito = Profile {displayName = "anon", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Nothing, peerType = Nothing, preferences = Nothing}
let incognito = Profile {displayName = "anon", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, simplexName = Nothing}
Profile {simplexName = sn} = userProfileDirect (testUser (Just testSimplexName)) (Just incognito) Nothing True
sn `shouldBe` Nothing
it "userProfileInGroup' passes simplexName through" $ do
let Profile {simplexName = sn} = userProfileInGroup' (testUser (Just testSimplexName)) True Nothing
sn `shouldBe` Just testSimplexName
it "redactedMemberProfile preserves simplexName" $ do
let p0 = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Nothing, contactLink = Nothing, simplexName = Just testSimplexName, peerType = Nothing, preferences = Nothing}
let p0 = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, simplexName = Just testSimplexName}
Profile {simplexName = sn} = redactedMemberProfile True p0
sn `shouldBe` Just testSimplexName
+2
View File
@@ -11,6 +11,7 @@ import ChatTests.DBUtils
import ChatTests.Utils (xdescribe'')
import Control.Logger.Simple
import Data.Time.Clock.System
import BadgeTests
import JSONTests
import MarkdownTests
import MemberRelationsTests
@@ -61,6 +62,7 @@ main = do
#endif
around tmpBracket $ describe "WebRTC encryption" webRTCTests
#endif
describe "Supporter badges" badgeTests
describe "SimpleX chat markdown" markdownTests
describe "JSON Tests" jsonTests
describe "Member relations" memberRelationsTests