core: test that history posted before channel signing reaches later joiners as signature missing, and channel moderator delete for all

Confirms the case the owner's new alert describes: a subscriber who joins after the owner turned on "Sign messages" sees the earlier posts as "signature missing" and later ones as signed, while earlier subscribers keep them unmarked. Also covers a non-owner channel moderator deleting a contributor's signed post for everyone.

Run: 001-channel-signing-moderators, slice 1
This commit is contained in:
spacedandy-agents-simplex committed 2026-10-23 10:37:17 +00:00
1 parent 773434a561
commit 98e4399775
1 file changed
+116 -8
+116 -8
View File
@@ -339,6 +339,7 @@ chatGroupTests = do
it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations
describe "promoted members roster" $ do
it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster
it "moderator deletes a contributor's message for all" testChannelModeratorDeleteForAll
it "subscriber recovers a missed roster member after a version gap" testChannelSubscriberRosterCatchUp
it "2 relays: subscriber recovers a missed roster member after a version gap" testChannel2RelaysSubscriberRosterCatchUp
it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster
@@ -384,6 +385,7 @@ chatGroupTests = do
it "should warn on missing signature when signing is required" testChannelSignMessagesRequired
it "should preserve signatures in history for catch-up subscribers" testChannelSignedHistory
it "should forward unsigned channel history for catch-up subscribers" testChannelUnsignedHistory
it "should mark history posted before signing was required as missing a signature" testChannelSignedAfterHistory
testGroupCheckMessages :: HasCallStack => TestParams -> IO ()
testGroupCheckMessages =
@@ -10879,6 +10881,62 @@ testChannelModeratorActionViaRoster ps =
DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text]
map (\(Only r) -> r) roles `shouldBe` [expectedRole]
testChannelModeratorDeleteForAll :: HasCallStack => TestParams -> IO ()
testChannelModeratorDeleteForAll ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan ->
withNewTestChat ps "eve" eveProfile $ \eve -> do
createChannel1Relay "team" alice bob cath dan eve
-- cath promoted observer -> moderator
threadDelay 1000000
alice ##> "/mr #team cath moderator"
alice <## "#team: you changed the role of cath to moderator (signed)"
concurrentlyN_
[ bob <## "#team: alice changed the role of cath from observer to moderator (signed)",
cath <## "#team: alice changed your role from observer to moderator (signed)",
dan <### [EndsWith "to moderator (signed)"],
eve <### [EndsWith "to moderator (signed)"]
]
-- promote dan (observer default) so it can post
threadDelay 1000000
promoteChannelMember "team" alice bob dan [cath, eve]
dan ##> "/_send #1 sign=on text hello from dan"
dan <# "#team hello from dan (signed)"
bob <# "#team dan> hello from dan (signed)"
concurrentlyN_
[ alice <# "#team dan> hello from dan (signed) [>>]",
do
cath <### [EndsWith "updated to dan"]
cath <## "#team: bob introduced dan (Daniel) in the channel"
cath <# "#team dan> hello from dan (signed) [>>]",
do
eve <### [EndsWith "updated to dan"]
eve <## "#team: bob introduced dan (Daniel) in the channel"
eve <# "#team dan> hello from dan (signed) [>>]"
]
-- cath (moderator) deletes dan's post for everyone; recipients holding it signed accept only a verified delete
threadDelay 1000000
danItemIdOnCath <- itemIdByText cath "hello from dan"
cath ##> ("/_delete member item #1 " <> danItemIdOnCath)
cath <## "message marked deleted by you"
concurrentlyN_
[ bob <# "#team dan> [marked deleted by cath] hello from dan (signed)",
alice <# "#team dan> [marked deleted by cath] hello from dan (signed)",
do
dan <### [EndsWith "updated to cath"]
dan <## "#team: bob introduced cath (Catherine) in the channel"
dan <# "#team dan> [marked deleted by cath] hello from dan (signed)",
do
eve <### [EndsWith "updated to cath"]
eve <## "#team: bob introduced cath (Catherine) in the channel"
eve <# "#team dan> [marked deleted by cath] hello from dan (signed)"
]
testChannelSubscriberRosterCatchUp :: HasCallStack => TestParams -> IO ()
testChannelSubscriberRosterCatchUp ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
@@ -13820,6 +13878,56 @@ testChannelUnsignedHistory ps =
dan #$> ("/_get chat #1 count=100 search=from cath member", chat, [(0, "from cath member")])
dan #$> ("/_get chat #1 count=100 search=history two", chat, [])
testChannelSignedAfterHistory :: HasCallStack => TestParams -> IO ()
testChannelSignedAfterHistory 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
-- as-group post before signing is required: unsigned
alice ##> "/_send #1(as_group=on) text before signing"
alice <# "#team before signing"
relay <# "#team> before signing"
[bob, cath] *<# "#team> before signing [>>]"
-- require signatures
alice ##> "/set signatures #team on"
alice <## "updated group preferences:"
alice <## "Sign messages: on"
concurrentlyN_
[ do
relay <## "alice updated group #team: (signed)"
relay <## "updated group preferences:"
relay <## "Sign messages: on",
do
bob <## "alice updated group #team: (signed)"
bob <## "updated group preferences:"
bob <## "Sign messages: on",
do
cath <## "alice updated group #team: (signed)"
cath <## "updated group preferences:"
cath <## "Sign messages: on"
]
-- as-group post after signing is required: auto-signed
alice ##> "/_send #1(as_group=on) text after signing"
alice <# "#team after signing (signed)"
relay <# "#team> after signing (signed)"
[bob, cath] *<# "#team> after signing (signed) [>>]"
-- a subscriber who received the earlier post before signing was required holds it unmarked
bob #$> ("/_get chat #1 count=100 search=before signing", chat, [(0, "before signing")])
-- catch-up subscriber joins late: the earlier post arrives unsigned while signing is required
memberJoinChannel "team" [relay] [alice] shortLink fullLink dan
dan <# "#team> before signing (signature missing) [>>]"
dan <# "#team> after signing (signed) [>>]"
dan #$> ("/_get chat #1 count=100 search=before signing", chat, [(0, "before signing (signature missing)")])
dan #$> ("/_get chat #1 count=100 search=after signing", chat, [(0, "after signing (signed)")])
testChannelAsGroupSpoof :: HasCallStack => TestParams -> IO ()
testChannelAsGroupSpoof ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
@@ -13997,14 +14105,14 @@ testChannelModerationDeleteSign ps =
dan <# "#team cath> [marked deleted by alice] moderated post (signed)",
eve <# "#team cath> [marked deleted by alice] moderated post (signed)"
]
where
itemIdByText :: TestCC -> T.Text -> IO String
itemIdByText cc t = do
rows <- withCCTransaction cc $ \db ->
DB.query db "SELECT chat_item_id FROM chat_items WHERE item_text LIKE '%' || ? || '%' ORDER BY chat_item_id DESC LIMIT 1" (Only t) :: IO [Only Int64]
case rows of
(Only i : _) -> pure (show i)
_ -> fail $ "no item with text " <> T.unpack t
itemIdByText :: TestCC -> T.Text -> IO String
itemIdByText cc t = do
rows <- withCCTransaction cc $ \db ->
DB.query db "SELECT chat_item_id FROM chat_items WHERE item_text LIKE '%' || ? || '%' ORDER BY chat_item_id DESC LIMIT 1" (Only t) :: IO [Only Int64]
case rows of
(Only i : _) -> pure (show i)
_ -> fail $ "no item with text " <> T.unpack t
testGroupLinkContentFilter :: HasCallStack => TestParams -> IO ()
testGroupLinkContentFilter =