{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} 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 import qualified Data.ByteString.Char8 as B 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.Messaging.Crypto.BBS (bbsKeyGen) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Util (decodeJSON) import Test.Hspec hiding (it) chatRelayTests :: SpecWith TestParams chatRelayTests = do describe "configure chat relays" $ do it "get and set chat relays" testGetSetChatRelays it "re-add soft-deleted relay by same address" testReAddRelaySameAddress it "re-add soft-deleted relay by same name" testReAddRelaySameName it "test chat relay" testChatRelayTest it "relay profile updated in address" testRelayProfileUpdateInAddress 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 = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do bob ##> "/ad" (bobSLink, _cLink) <- getContactLinks bob True cath ##> "/ad" (cathSLink, _cLink) <- getContactLinks cath True alice ##> ("/relays name=bob_relay " <> bobSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <## (" bob_relay: " <> bobSLink) alice ##> ("/relays name=cath_relay " <> cathSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <## (" cath_relay: " <> cathSLink) alice ##> ("/relays name=bob_relay " <> bobSLink <> " name=cath_relay " <> cathSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <### [ ConsoleString $ " bob_relay: " <> bobSLink, ConsoleString $ " cath_relay: " <> cathSLink ] -- Relay used by a channel is soft-deleted (referenced in group_relays). -- Re-adding with same address should un-delete it. testReAddRelaySameAddress :: HasCallStack => TestParams -> IO () testReAddRelaySameAddress ps = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do bob ##> "/ad" (bobSLink, _cLink) <- getContactLinks bob True cath ##> "/ad" (cathSLink, _cLink) <- getContactLinks cath True -- Configure bob as relay and create channel (creates group_relays reference) alice ##> ("/relays name=bob_relay " <> bobSLink) alice <## "ok" createChannelWithRelay "team" alice bob -- Replace bob_relay with cath_relay (bob_relay is soft-deleted, referenced in group_relays) alice ##> ("/relays name=cath_relay " <> cathSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <## (" cath_relay: " <> cathSLink) -- Re-add with same address but different name - should succeed (un-deletes soft-deleted row by address) alice ##> ("/relays name=bob_relay2 " <> bobSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <## (" bob_relay2: " <> bobSLink) -- Relay used by a channel is soft-deleted (referenced in group_relays). -- Re-adding with same name and same address should un-delete it. testReAddRelaySameName :: HasCallStack => TestParams -> IO () testReAddRelaySameName ps = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do bob ##> "/ad" (bobSLink, _cLink) <- getContactLinks bob True cath ##> "/ad" (cathSLink, _cLink) <- getContactLinks cath True -- Configure bob as relay named "my_relay" and create channel alice ##> ("/relays name=my_relay " <> bobSLink) alice <## "ok" createChannelWithRelay "team" alice bob -- Replace with cath_relay (my_relay is soft-deleted) alice ##> ("/relays name=cath_relay " <> cathSLink) alice <## "ok" -- Re-add with same name and same address - should succeed (un-deletes by address match) alice ##> ("/relays name=my_relay " <> bobSLink) alice <## "ok" alice ##> "/relays" alice <## "Your servers" alice <## " Chat relays" alice <## (" my_relay: " <> bobSLink) testChatRelayTest :: HasCallStack => TestParams -> IO () testChatRelayTest ps = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do -- Setup: bob (relay) creates address bob ##> "/ad" (bobSLink, _cLink) <- getContactLinks bob True -- Setup: cath (normal user) creates address cath ##> "/ad" (cathSLink, _cLink) <- getContactLinks cath True -- Scenario 1: Happy path - test relay address succeeds alice ##> ("/relay test " <> bobSLink) alice <## "relay test passed, profile: bob (Bob)" -- Scenario 2: Non-relay address - cath is not a relay user, -- her address has ContactShortLinkData, not RelayAddressLinkData alice ##> ("/relay test " <> cathSLink) alice <##. "relay test failed at RTSDecodeLink, error: " -- Scenario 3: Deleted address - bob deletes his address bob ##> "/da" bob <## "Your chat address is deleted - accepted contacts will remain connected." bob <## "To create a new chat address use /ad" alice ##> ("/relay test " <> bobSLink) alice <##. "relay test failed at RTSGetLink, error: " testRelayProfileUpdateInAddress :: HasCallStack => TestParams -> IO () testRelayProfileUpdateInAddress ps = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do bob ##> "/ad" (bobSLink, _cLink) <- getContactLinks bob True alice ##> ("/relay test " <> bobSLink) alice <## "relay test passed, profile: bob (Bob)" bob ##> "/p bob2 Bob relay" bob <## "user profile is changed to bob2 (Bob relay) (your 0 contacts are notified)" threadDelay 100000 alice ##> ("/relay test " <> bobSLink) alice <## "relay test passed, profile: bob2 (Bob relay)" testShareChannelDirect :: HasCallStack => TestParams -> IO () testShareChannelDirect ps = testChat3 aliceProfile bobProfile cathProfile test ps where test alice bob cath = withRelay ps $ \relay -> do (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay connectUsers alice bob -- alice gets ownerSig from share content API (for validation later) alice ##> "/_share chat content #1 @2" alice <## "link to join channel #news (signed):" (_, apiOwnerSig) <- getTermLine2 alice -- alice sends the card to bob alice ##> "/share chat #news @bob" alice <# "@bob link to join channel #news (signed):" _ <- getTermLine2 alice -- alice's testView ownerSig bob <# "alice> link to join channel #news (signed):" -- bob captures the received ownerSig from message view (testView) (sLink, cSig) <- getTermLine2 bob sLink `shouldBe` shortLink cSig `shouldBe` apiOwnerSig -- bob verifies owner signature via connect plan bob ##> ("/_connect plan 1 " <> shortLink <> " sig=" <> cSig) bob <## "group link: ok to connect via relays" bob <## "owner signature: verified" _ <- getTermLine bob -- group link data -- bob joins memberJoinChannel' "news" 1 0 1 0 [relay] [alice] shortLink fullLink bob connectUsers bob cath -- bob (subscriber) shares unsigned - not owner bob ##> "/share chat #news @cath" bob <# "@cath link to join channel #news:" _ <- getTermLine bob cath <# "bob> link to join channel #news:" _ <- getTermLine cath -- bob tries to replay alice's signed card to cath - binding mismatch, sig stripped at receive let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) cLink = either error id $ strDecode (B.pack sLink) mc = MCChat (T.pack sLink) (MCLGroup cLink (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" bob ##> ("/_send @3 json [" <> cm <> "]") bob <# "@cath link to join group #news (signed):" _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) -- cath sees it without signature - binding was for alice->bob, not bob->cath, sig stripped cath <# "bob> link to join group #news:" _ <- getTermLine cath -- cath joins anyway memberJoinChannel "news" [relay] [alice] shortLink fullLink cath alice #> "#news hello" relay <# "#news> hello" [bob, cath] *<# "#news> hello [>>]" testShareChannelGroup :: HasCallStack => TestParams -> IO () testShareChannelGroup ps = testChat3 aliceProfile bobProfile cathProfile test ps where test alice bob cath = withRelay ps $ \relay -> do (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay createGroup2 "team" alice bob alice ##> "/share chat #news #team" alice <# "#team link to join channel #news:" _ <- getTermLine alice bob <# "#team alice> link to join channel #news:" sLink <- getTermLine bob sLink `shouldBe` shortLink memberJoinChannel' "news" 2 0 1 0 [relay] [alice] sLink fullLink bob createGroup2 "work" bob cath bob ##> "/share chat #news #work" bob <# "#work link to join channel #news:" _ <- getTermLine bob cath <# "#work bob> link to join channel #news:" _ <- getTermLine cath memberJoinChannel' "news" 2 0 0 0 [relay] [alice] shortLink fullLink cath alice #> "#news hello" relay <# "#news> hello" [bob, cath] *<# "#news> hello [>>]" testShareChannelChannel :: HasCallStack => TestParams -> IO () testShareChannelChannel ps = testChat3 aliceProfile bobProfile cathProfile test ps where test alice bob cath = withRelay ps $ \relay -> do relaySLink <- setupRelay alice relay (sLink1, fLink1) <- prepareChannel "news" alice relay (sLink2, fLink2) <- prepareChannel' 2 "updates" alice relay -- bob joins "updates" first (relay doesn't know bob yet, no suffix) memberJoinChannel "updates" [relay] [alice] sLink2 fLink2 bob -- alice (owner) shares "news" to "updates" - signed alice ##> "/_share chat content #1 #2(as_group=on)" alice <## "link to join channel #news (signed):" (apiLink, apiOwnerSig) <- getTermLine2 alice apiLink `shouldBe` sLink1 alice ##> "/share chat #news #updates" alice <# "#updates link to join channel #news (signed):" _ <- getTermLine2 alice -- link, ownerSig relay <# "#updates> link to join channel #news (signed):" _ <- getTermLine2 relay -- link, ownerSig bob <# "#updates> link to join channel #news (signed): [>>]" (cLink, cSig) <- getTermLine2 bob cLink `shouldBe` (sLink1 <> " [>>]") cSig `shouldBe` apiOwnerSig -- bob verifies alice's signature via connect plan bob ##> ("/_connect plan 1 " <> sLink1 <> " sig=" <> cSig) bob <## "group link: ok to connect via relays" bob <## "owner signature: verified" _ <- getTermLine bob -- group link data -- bob joins "news" (group #2 for bob, relay knows bob from "updates" so sfx=1) memberJoinChannel' "news" 2 1 1 1 [relay] [alice] sLink1 fLink1 bob -- bob creates channel "bob_ch" for delivery to cath bob ##> ("/relays name=relay " <> relaySLink) bob <## "ok" (sLink3, fLink3) <- prepareChannel "bob_ch" bob relay memberJoinChannel "bob_ch" [relay] [bob] sLink3 fLink3 cath -- bob (subscriber) shares "news" to "bob_ch" - unsigned (not owner) bob ##> "/share chat #news #bob_ch" bob <# "#bob_ch link to join channel #news:" _ <- getTermLine bob relay <# "#bob_ch> link to join channel #news:" _ <- getTermLine relay cath <# "#bob_ch> link to join channel #news: [>>]" _ <- getTermLine cath -- bob tries to replay alice's signed card to bob_ch - binding mismatch, sig stripped at receive let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) cLink' = either error id $ strDecode (B.pack sLink1) mc = MCChat (T.pack sLink1) (MCLGroup cLink' (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" bob ##> ("/_send #3 json [" <> cm <> "]") bob <# "#bob_ch link to join group #news (signed):" _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) relay <# "#bob_ch bob_2> link to join group #news:" _ <- getTermLine relay cath <# "#bob_ch bob> link to join group #news: [>>]" _ <- getTermLine cath -- cath joins "news" (group #2 for cath since "bob_ch" is #1) memberJoinChannel' "news" 2 1 0 1 [relay] [alice] sLink1 fLink1 cath -- alice sends message, both receive alice #> "#news hello" relay <# "#news> hello" [bob, cath] *<# "#news> hello [>>]" getTermLine2 :: TestCC -> IO (String, String) getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c -- Create a public group with relay=1, wait for relay to join createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO () createChannelWithRelay 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 <## " - relay id 1: active" owner <## "group link:" _ <- getTermLine owner pure (), relay <## ("#" <> gName <> ": you joined the group as relay") ]