Merge branch 'master' into f/channel-comments

This commit is contained in:
spaced4ndy
2026-05-18 14:13:11 +04:00
105 changed files with 4409 additions and 941 deletions
-1
View File
@@ -7,7 +7,6 @@
module Bots.DirectoryTests where
import ChatClient
import ChatTests.ChatRelays (withRelay)
import ChatTests.DBUtils
import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay)
import ChatTests.Utils
-3
View File
@@ -325,9 +325,6 @@ testShareChannelChannel ps =
getTermLine2 :: TestCC -> IO (String, String)
getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c
withRelay :: HasCallStack => TestParams -> (TestCC -> IO ()) -> IO ()
withRelay ps = withNewTestChatOpts ps relayTestOpts "relay" relayProfile
-- Create a public group with relay=1, wait for relay to join
createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO ()
createChannelWithRelay gName owner relay = do
+316 -10
View File
@@ -272,9 +272,15 @@ chatGroupTests = do
it "should add relay to existing channel" testChannelAddRelay
it "should remove relay from channel" testChannelRemoveRelay
it "should remove left relay from channel" testChannelRemoveLeftRelay
describe "relay rejection" $ do
it "relay rejects fresh invitation after leaving the same channel" testRelayRejectAfterLeave
it "operator allow clears rejection and relay accepts again" testRelayAllowAcceptsAgain
it "rejection on channel A does not affect unrelated channel B" testRelayDoesNotRejectUnrelatedChannel
it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations
describe "channel message operations" $ do
it "should update channel message" testChannelMessageUpdate
it "should delete channel message" testChannelMessageDelete
it "should delete channel message from history" testChannelMessageDeleteFromHistory
it "should send and receive channel message file" testChannelMessageFile
it "should cancel channel message file" testChannelMessageFileCancel
it "should quote channel message" testChannelMessageQuote
@@ -8470,7 +8476,7 @@ testSupportPreferenceGroup =
testSupportPreferenceChannel :: HasCallStack => TestParams -> IO ()
testSupportPreferenceChannel ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay ->
withNewTestChatOpts ps relayTestOpts "relay" chatRelayProfile $ \relay ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice relay
@@ -9486,8 +9492,9 @@ testChannelRelayLeave ps =
-- relay1 (bob) leaves
threadDelay 100000
bob ##> "/leave #team"
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group"
bob <## "#team: you left the group (future invitations will be rejected)"
bob <## "use /group allow #team to allow future invitations"
bob <## "use /d #team to delete the group (also clears the rejection)"
concurrentlyN_
[ alice <## "#team: bob left the group (signed)",
-- cath: not notified (relays not connected, owner doesn't forward)
@@ -9509,8 +9516,9 @@ testChannelRelayLeave ps =
-- relay2 (cath) leaves
threadDelay 100000
cath ##> "/leave #team"
cath <## "#team: you left the group"
cath <## "use /d #team to delete the group"
cath <## "#team: you left the group (future invitations will be rejected)"
cath <## "use /group allow #team to allow future invitations"
cath <## "use /d #team to delete the group (also clears the rejection)"
concurrentlyN_
[ alice <## "#team: cath left the group (signed)",
dan <## "#team: cath left the group (signed)",
@@ -9881,8 +9889,9 @@ testChannelRemoveLeftRelay ps =
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
bob <## "#team: you left the group (future invitations will be rejected)"
bob <## "use /group allow #team to allow future invitations"
bob <## "use /d #team to delete the group (also clears the rejection)",
alice <## "#team: bob left the group (signed)",
dan <## "#team: bob left the group (signed)"
]
@@ -9910,8 +9919,9 @@ testChannelRemoveLeftRelay ps =
cath ##> "/l team"
concurrentlyN_
[ do
cath <## "#team: you left the group"
cath <## "use /d #team to delete the group",
cath <## "#team: you left the group (future invitations will be rejected)"
cath <## "use /group allow #team to allow future invitations"
cath <## "use /d #team to delete the group (also clears the rejection)",
alice <## "#team: cath left the group (signed)",
dan <## "#team: cath left the group (signed)"
]
@@ -9933,6 +9943,271 @@ testChannelRemoveLeftRelay ps =
DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text]
danMembers2 `shouldMatchList` [Only "dan", Only "alice"]
queryRelayOwnStatus :: TestCC -> Int64 -> IO (Maybe T.Text)
queryRelayOwnStatus cc gId = do
rows <- withCCTransaction cc $ \db ->
DB.query db "SELECT relay_own_status FROM groups WHERE group_id = ?" (Only gId)
:: IO [Only (Maybe T.Text)]
pure $ case rows of
[Only s] -> s
_ -> Nothing
listRelayOwnStatuses :: TestCC -> IO [(Int64, T.Text)]
listRelayOwnStatuses cc =
withCCTransaction cc $ \db ->
DB.query_
db
"SELECT group_id, relay_own_status FROM groups WHERE relay_own_status IS NOT NULL ORDER BY group_id"
:: IO [(Int64, T.Text)]
checkRelayGroupCount :: TestCC -> Int -> IO ()
checkRelayGroupCount cc expected = do
rows <- withCCTransaction cc $ \db ->
DB.query_ db "SELECT COUNT(*) FROM groups WHERE relay_own_status IS NOT NULL" :: IO [Only Int]
let n = case rows of
[Only c] -> c
_ -> 0
n `shouldBe` expected
testRelayRejectAfterLeave :: HasCallStack => TestParams -> IO ()
testRelayRejectAfterLeave ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
threadDelay 100000
-- baseline: subscriber receives forwarded messages via the active relay
alice #> "#team hello"
bob <# "#team> hello"
cath <# "#team> hello [>>]"
-- relay leaves the channel: subscriber gets the signed leave notice via bob's
-- DJRelayRemoved job, then has no relay to forward subsequent messages.
bob ##> "/leave #team"
bob <## "#team: you left the group (future invitations will be rejected)"
bob <## "use /group allow #team to allow future invitations"
bob <## "use /d #team to delete the group (also clears the rejection)"
concurrentlyN_
[ alice <## "#team: bob left the group (signed)",
cath <## "#team: bob left the group (signed)"
]
threadDelay 100000
bobLeaveStatus <- queryRelayOwnStatus bob 1
bobLeaveStatus `shouldBe` Just "rejected"
-- with no active relay, owner's messages don't reach the subscriber
alice #> "#team after leave"
(cath </)
-- owner removes the (now-left) relay member; cascade clears alice's group_relays row
alice ##> "/rm #team bob"
alice <## "#team: you removed bob from the group (signed)"
threadDelay 100000
-- owner re-adds bob as relay
alice ##> "/_add relays #1 1"
alice <## "#team: group relays:"
alice .<##. (" - relay id", ": invited")
-- bob's xGrpRelayInv finds the 'rejected' row for this link and sends XGrpRelayReject.
-- alice's CONF handler emits TERelayRejected; the relay row flips to 'rejected'.
alice <## "#team: relay rejected, reason: RRRRejoinRejected"
-- assert alice's fresh GroupRelay row is marked 'rejected' and the relay
-- GroupMember is GSMemLeft so the owner UI treats it as gone
aliceRelayStatuses <- withCCTransaction alice $ \db ->
DB.query_ db "SELECT relay_status FROM group_relays" :: IO [Only T.Text]
map (\(Only s) -> s) aliceRelayStatuses `shouldBe` ["rejected"]
aliceRelayMemStatuses <- withCCTransaction alice $ \db ->
DB.query_ db "SELECT member_status FROM group_members WHERE member_role = 'relay'"
:: IO [Only T.Text]
map (\(Only s) -> s) aliceRelayMemStatuses `shouldBe` ["left"]
-- subscriber still doesn't receive after the failed re-invitation
alice #> "#team after rejection"
(cath </)
-- bob's transient row was created with relay_own_status='rejected';
-- after INFO arrives the cleanup arm deletes it. Original row 1 remains rejected.
threadDelay 1000000
checkRelayGroupCount bob 1
finalStatuses <- listRelayOwnStatuses bob
finalStatuses `shouldBe` [(1, "rejected")]
testRelayAllowAcceptsAgain :: HasCallStack => TestParams -> IO ()
testRelayAllowAcceptsAgain ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
threadDelay 100000
-- baseline: subscriber receives forwarded messages
alice #> "#team hello"
bob <# "#team> hello"
cath <# "#team> hello [>>]"
bob ##> "/leave #team"
bob <## "#team: you left the group (future invitations will be rejected)"
bob <## "use /group allow #team to allow future invitations"
bob <## "use /d #team to delete the group (also clears the rejection)"
concurrentlyN_
[ alice <## "#team: bob left the group (signed)",
cath <## "#team: bob left the group (signed)"
]
threadDelay 100000
-- with no relay, subscriber doesn't receive
alice #> "#team during downtime"
(cath </)
-- /_relay allow flips bob's row from 'rejected' to 'inactive'
bob ##> "/group allow #team"
bob <## "#team: relay rejection cleared"
bobClearStatus <- queryRelayOwnStatus bob 1
bobClearStatus `shouldBe` Just "inactive"
-- owner can now re-add and bob accepts as relay (the rejection has been cleared)
alice ##> "/rm #team bob"
alice <## "#team: you removed bob from the group (signed)"
threadDelay 100000
alice ##> "/_add relays #1 1"
concurrentlyN_
[ do
alice <## "#team: group relays:"
alice .<##. (" - relay id", ": invited")
alice <## "#team: group link relays updated, current relays:"
alice .<##. (" - relay id", ": active")
alice <## "group link:"
void $ getTermLine alice,
bob <## "#team_1: you joined the group as relay"
]
threadDelay 100000
-- subscriber syncs against link data and reconnects to the new relay
cath ##> "/_get group link data #1"
cath <## "group ID: 1"
void $ getTermLine cath
concurrentlyN_
[ do
cath <## "#team: joining the group (connecting to relay bob)..."
cath <## "#team: you joined the group (connected to relay bob)",
do
bob <## "cath_1 (Catherine): accepting request to join group #team_1..."
bob <## "#team_1: cath_1 joined the group"
]
threadDelay 100000
-- delivery resumes through the freshly accepted relay
alice #> "#team after allow"
bob <# "#team_1> after allow"
cath <# "#team> after allow [>>]"
-- after re-acceptance, the relay GroupMember is not in the rejected/left state
aliceRelayMemStatuses <- withCCTransaction alice $ \db ->
DB.query_ db "SELECT member_status FROM group_members WHERE member_role = 'relay'"
:: IO [Only T.Text]
map (\(Only s) -> s) aliceRelayMemStatuses `shouldBe` ["connected"]
testRelayDoesNotRejectUnrelatedChannel :: HasCallStack => TestParams -> IO ()
testRelayDoesNotRejectUnrelatedChannel ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
_ <- prepareChannel1Relay "teama" alice bob
threadDelay 100000
bob ##> "/leave #teama"
bob <## "#teama: you left the group (future invitations will be rejected)"
bob <## "use /group allow #teama to allow future invitations"
bob <## "use /d #teama to delete the group (also clears the rejection)"
alice <## "#teama: bob left the group (signed)"
threadDelay 100000
bobAStatus <- queryRelayOwnStatus bob 1
bobAStatus `shouldBe` Just "rejected"
-- alice creates a second channel reusing the same bob relay config.
-- bob's xGrpRelayInv for teamb's link finds no rejection and accepts normally.
(shortLinkB, fullLinkB) <- prepareChannel' 2 "teamb" alice bob
memberJoinChannel "teamb" [bob] [alice] shortLinkB fullLinkB cath
threadDelay 100000
-- subscriber on teamb receives forwarded messages, proving bob accepts teamb
-- even though teama remains rejected on bob's side.
alice #> "#teamb hello"
bob <# "#teamb> hello"
cath <# "#teamb> hello [>>]"
bobBStatus <- queryRelayOwnStatus bob 2
bobBStatus `shouldNotBe` Just "rejected"
bobBStatus `shouldNotBe` Nothing
testRelayRejectRaceConcurrentInvitations :: HasCallStack => TestParams -> IO ()
testRelayRejectRaceConcurrentInvitations ps =
-- After rejection, multiple sequential re-invitations must all reject with
-- consistent state (each transient row created with RSRejected and cleaned
-- up by its own INFO).
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
threadDelay 100000
-- baseline: subscriber receives forwarded messages
alice #> "#team hello"
bob <# "#team> hello"
cath <# "#team> hello [>>]"
bob ##> "/leave #team"
bob <## "#team: you left the group (future invitations will be rejected)"
bob <## "use /group allow #team to allow future invitations"
bob <## "use /d #team to delete the group (also clears the rejection)"
concurrentlyN_
[ alice <## "#team: bob left the group (signed)",
cath <## "#team: bob left the group (signed)"
]
threadDelay 100000
-- first rejection
alice ##> "/rm #team bob"
alice .<##. ("#team: you removed bob from the group", "")
threadDelay 100000
alice ##> "/_add relays #1 1"
alice <## "#team: group relays:"
alice .<##. (" - relay id", ": invited")
alice <## "#team: relay rejected, reason: RRRRejoinRejected"
threadDelay 1000000
checkRelayGroupCount bob 1
-- subscriber doesn't receive between rejections (no active relay)
alice #> "#team between rejections"
(cath </)
-- second rejection
alice ##> "/rm #team bob"
alice .<##. ("#team: you removed bob from the group", "")
threadDelay 100000
alice ##> "/_add relays #1 1"
alice <## "#team: group relays:"
alice .<##. (" - relay id", ": invited")
alice <## "#team: relay rejected, reason: RRRRejoinRejected"
-- subscriber still doesn't receive after the second rejection
alice #> "#team after second rejection"
(cath </)
threadDelay 1000000
checkRelayGroupCount bob 1
finalStatuses <- listRelayOwnStatuses bob
finalStatuses `shouldBe` [(1, "rejected")]
testChannelCreateDeletedRelay :: HasCallStack => TestParams -> IO ()
testChannelCreateDeletedRelay ps =
withNewTestChat ps "alice" aliceProfile $ \alice -> do
@@ -9963,7 +10238,7 @@ testChannelCreateDeletedRelay ps =
testChannelSupportScope :: HasCallStack => TestParams -> IO ()
testChannelSupportScope ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay ->
withNewTestChatOpts ps relayTestOpts "relay" chatRelayProfile $ \relay ->
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice relay
@@ -10040,6 +10315,37 @@ testChannelMessageDelete ps =
bob <# "#team> [marked deleted] hello"
[cath, dan, eve] *<# "#team> [marked deleted] hello" -- TODO show as forwarded
testChannelMessageDeleteFromHistory :: HasCallStack => TestParams -> IO ()
testChannelMessageDeleteFromHistory ps =
testChat4 aliceProfile bobProfile cathProfile danProfile test ps
where
test alice bob cath dan = withRelay ps $ \relay -> do
(shortLink, fullLink) <- prepareChannel1Relay "team" alice relay
memberJoinChannel "team" [relay] [alice] shortLink fullLink bob
memberJoinChannel "team" [relay] [alice] shortLink fullLink cath
alice #> "#team hello"
relay <# "#team> hello"
[bob, cath] *<# "#team> hello [>>]"
-- owner deletes from history (relay processes locally but doesn't forward)
msgId <- lastItemId alice
alice #$> ("/_delete item #1 " <> msgId <> " history", id, "message marked deleted")
relay <# "#team> [marked deleted] hello"
-- subscribers don't receive deletion - next message arrives cleanly
alice #> "#team still here"
relay <# "#team> still here"
[bob, cath] *<# "#team> still here [>>]"
-- internal delete rejected for channel owner
msgId2 <- lastItemId alice
alice ##> ("/_delete item #1 " <> msgId2 <> " internal")
alice <## "cannot delete this item"
memberJoinChannel "team" [relay] [alice] shortLink fullLink dan
dan <# "#team> still here [>>]"
testChannelMessageFile :: HasCallStack => TestParams -> IO ()
testChannelMessageFile ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
+5 -2
View File
@@ -81,8 +81,8 @@ frankProfile = mkProfile "frank" "Frank" Nothing
businessProfile :: Profile
businessProfile = mkProfile "biz" "Biz Inc" Nothing
relayProfile :: Profile
relayProfile = mkProfile "relay" "Relay" Nothing
chatRelayProfile :: Profile
chatRelayProfile = mkProfile "relay" "Relay" Nothing
mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs}
@@ -149,6 +149,9 @@ runTestCfg3 aliceCfg bobCfg cathCfg runTest ps =
withNewTestChatCfg ps cathCfg "cath" cathProfile $ \cath ->
runTest alice bob cath
withRelay :: HasCallStack => TestParams -> (TestCC -> IO ()) -> IO ()
withRelay ps = withNewTestChatOpts ps relayTestOpts "relay" chatRelayProfile
-- | test sending direct messages
(<##>) :: HasCallStack => TestCC -> TestCC -> IO ()
cc1 <##> cc2 = do
+4
View File
@@ -227,6 +227,10 @@ textWithUri = describe "text with Uri" do
"https://github.com/simplex-chat/ - SimpleX on GitHub" <==> uri "https://github.com/simplex-chat/" <> " - SimpleX on GitHub"
-- "SimpleX on GitHub (https://github.com/simplex-chat/)" <==> "SimpleX on GitHub (" <> uri "https://github.com/simplex-chat/" <> ")"
"https://en.m.wikipedia.org/wiki/Servo_(software)" <==> uri "https://en.m.wikipedia.org/wiki/Servo_(software)"
"https://simplex.chat/page_name_" <==> uri "https://simplex.chat/page_name_"
"https://simplex.chat/page_name_, hello" <==> uri "https://simplex.chat/page_name_" <> ", hello"
"https://simplex.chat/page!" <==> uri "https://simplex.chat/page!"
"https://simplex.chat/page!, hello" <==> uri "https://simplex.chat/page!" <> ", hello"
"example.com" <==> uri "example.com"
"example.com." <==> uri "example.com" <> "."
"example.com..." <==> uri "example.com" <> "..."
+1 -1
View File
@@ -211,7 +211,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing Nothing Nothing (Just MsgPrefs {commentsDisabled = True})
it "x.msg.del" $
"{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
#==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing Nothing
#==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing Nothing False
it "x.msg.deleted" $
"{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted