-
Notifications
You must be signed in to change notification settings - Fork 334
[FS-901] Tests for joining subconversation #2974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
a98be1e
f12b5cd
352e063
94cfea3
64d5e09
dc2d662
be36385
8144b3e
97d91da
dd20fee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add more tests for joining a subconversation |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,8 @@ where | |
| import Control.Arrow ((>>>)) | ||
| import Control.Comonad | ||
| import Control.Error.Util (hush) | ||
| import Control.Lens (preview) | ||
| import Control.Lens (forOf_, preview) | ||
| import Control.Lens.Extras (is) | ||
| import Data.Id | ||
| import Data.Json.Util | ||
| import Data.List.NonEmpty (NonEmpty, nonEmpty) | ||
|
|
@@ -785,11 +786,10 @@ processExternalCommit qusr mSenderClient lConvOrSub epoch action updatePath = wi | |
| throw . mlsProtocolError $ | ||
| "The external commit attempts to add another client of the user, it must only add itself" | ||
|
|
||
| case convOrSub of | ||
| Conv _ -> pure () | ||
| SubConv mlsConv _ -> | ||
| unless (isClientMember cid (mcMembers mlsConv)) $ | ||
| throwS @'MLSSubConvClientNotInParent | ||
| -- only members can join a subconversation | ||
| forOf_ _SubConv convOrSub $ \(mlsConv, _) -> | ||
| unless (isClientMember cid (mcMembers mlsConv)) $ | ||
| throwS @'MLSSubConvClientNotInParent | ||
|
|
||
| -- check if there is a key package ref in the remove proposal | ||
| remRef <- | ||
|
|
@@ -982,7 +982,7 @@ processInternalCommit qusr senderClient con lConvOrSub epoch action senderRef co | |
| throwS @'MLSCommitMissingReferences | ||
|
|
||
| -- process and execute proposals | ||
| updates <- executeProposalAction lConvOrSub qusr con convOrSub action | ||
| updates <- executeProposalAction qusr con lConvOrSub action | ||
|
|
||
| -- update key package ref if necessary | ||
| postponedKeyPackageRefUpdate | ||
|
|
@@ -1218,8 +1218,7 @@ checkExternalProposalUser qusr prop = do | |
| (const $ pure ()) -- FUTUREWORK: check external proposals from remote backends | ||
| qusr | ||
|
|
||
| executeProposalAction :: | ||
| forall r x. | ||
| type HasProposalActionEffects r = | ||
| ( Member BrigAccess r, | ||
| Member ConversationStore r, | ||
| Member (Error InternalError) r, | ||
|
|
@@ -1244,44 +1243,51 @@ executeProposalAction :: | |
| Member TinyLog r, | ||
| CallsFed 'Galley "on-conversation-updated", | ||
| CallsFed 'Galley "on-mls-message-sent", | ||
| CallsFed 'Galley "on-new-remote-conversation", | ||
| CallsFed 'Galley "on-new-remote-conversation" | ||
| ) | ||
|
|
||
| executeProposalAction :: | ||
| forall r. | ||
| ( HasProposalActionEffects r, | ||
| CallsFed 'Brig "get-mls-clients" | ||
| ) => | ||
| Local x -> | ||
| Qualified UserId -> | ||
| Maybe ConnId -> | ||
| ConvOrSubConv -> | ||
| Local ConvOrSubConv -> | ||
| ProposalAction -> | ||
| Sem r [LocalConversationUpdate] | ||
| executeProposalAction _loc _qusr _con (SubConv _ _) _action = pure [] | ||
| executeProposalAction loc qusr con (Conv mlsConv) action = do | ||
| let lconv = qualifyAs loc . mcConv $ mlsConv | ||
| mlsMeta = mcMLSData mlsConv | ||
| cm = mcMembers mlsConv | ||
| executeProposalAction qusr con lconvOrSub action = do | ||
| let convOrSub = tUnqualified lconvOrSub | ||
| mlsMeta = mlsMetaConvOrSub convOrSub | ||
| cm = membersConvOrSub convOrSub | ||
| ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) | ||
| newUserClients = Map.assocs (paAdd action) | ||
|
|
||
| -- no client can be directly added to a subconversation | ||
| when (is _SubConv convOrSub && not (null newUserClients)) $ | ||
| throw (mlsProtocolError "Add proposals in subconversations are not supported") | ||
|
|
||
| -- Note [client removal] | ||
| -- We support two types of removals: | ||
| -- 1. when a user is removed from a group, all their clients have to be removed | ||
| -- 2. when a client is deleted, that particular client (but not necessarily | ||
| -- other clients of the same user), has to be removed. | ||
| -- other clients of the same user) has to be removed. | ||
| -- | ||
| -- Type 2 requires no special processing on the backend, so here we filter | ||
| -- out all removals of that type, so that further checks and processing can | ||
| -- be applied only to type 1 removals. | ||
| removedUsers <- mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ | ||
| \(qtarget, Map.keysSet -> clients) -> runError @() $ do | ||
| -- fetch clients from brig | ||
| clientInfo <- Set.map ciId <$> getClientInfo lconv qtarget ss | ||
| clientInfo <- Set.map ciId <$> getClientInfo lconvOrSub qtarget ss | ||
| -- if the clients being removed don't exist, consider this as a removal of | ||
| -- type 2, and skip it | ||
| when (Set.null (clientInfo `Set.intersection` clients)) $ | ||
| throw () | ||
| pure (qtarget, clients) | ||
|
|
||
| -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 | ||
| foldQualified lconv (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr | ||
| foldQualified lconvOrSub (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth it having a different combinator for these cases, like |
||
|
|
||
| -- for each user, we compare their clients with the ones being added to the conversation | ||
| for_ newUserClients $ \(qtarget, newclients) -> case Map.lookup qtarget cm of | ||
|
|
@@ -1292,7 +1298,7 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do | |
| -- final set of clients in the conversation | ||
| let clients = Map.keysSet (newclients <> Map.findWithDefault mempty qtarget cm) | ||
| -- get list of mls clients from brig | ||
| clientInfo <- getClientInfo lconv qtarget ss | ||
| clientInfo <- getClientInfo lconvOrSub qtarget ss | ||
| let allClients = Set.map ciId clientInfo | ||
| let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) | ||
| -- We check the following condition: | ||
|
|
@@ -1316,14 +1322,21 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do | |
| membersToRemove <- catMaybes <$> for removedUsers (uncurry (checkRemoval cm)) | ||
|
|
||
| -- add users to the conversation and send events | ||
| addEvents <- foldMap (addMembers lconv) . nonEmpty . map fst $ newUserClients | ||
| addEvents <- | ||
| foldMap (addMembers qusr con lconvOrSub) | ||
| . nonEmpty | ||
| . map fst | ||
| $ newUserClients | ||
|
|
||
| -- add clients in the conversation state | ||
| for_ newUserClients $ \(qtarget, newClients) -> do | ||
| addMLSClients (cnvmlsGroupId mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) | ||
|
|
||
| -- remove users from the conversation and send events | ||
| removeEvents <- foldMap (removeMembers lconv) (nonEmpty membersToRemove) | ||
| removeEvents <- | ||
| foldMap | ||
| (removeMembers qusr con lconvOrSub) | ||
| (nonEmpty membersToRemove) | ||
|
|
||
| -- Remove clients from the conversation state. This includes client removals | ||
| -- of all types (see Note [client removal]). | ||
|
|
@@ -1346,43 +1359,63 @@ executeProposalAction loc qusr con (Conv mlsConv) action = do | |
| throwS @'MLSSelfRemovalNotAllowed | ||
| pure (Just qtarget) | ||
|
|
||
| existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingLocalMembers lconv = | ||
| (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) | ||
|
|
||
| existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingRemoteMembers lconv = | ||
| Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ | ||
| lconv | ||
|
|
||
| existingMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv | ||
|
|
||
| addMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] | ||
| addMembers lconv = | ||
| -- FUTUREWORK: update key package ref mapping to reflect conversation membership | ||
| foldMap | ||
| ( handleNoChanges | ||
| . handleMLSProposalFailures @ProposalErrors | ||
| . fmap pure | ||
| . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con | ||
| . flip ConversationJoin roleNameWireMember | ||
| ) | ||
| . nonEmpty | ||
| . filter (flip Set.notMember (existingMembers lconv)) | ||
| . toList | ||
|
|
||
| removeMembers :: Local Data.Conversation -> NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] | ||
| removeMembers lconv = | ||
| foldMap | ||
| ( handleNoChanges | ||
| . handleMLSProposalFailures @ProposalErrors | ||
| . fmap pure | ||
| . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con | ||
| ) | ||
| . nonEmpty | ||
| . filter (flip Set.member (existingMembers lconv)) | ||
| . toList | ||
| existingLocalMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingLocalMembers lconv = | ||
| (Set.fromList . map (fmap lmId . tUntagged)) (traverse convLocalMembers lconv) | ||
|
|
||
| existingRemoteMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingRemoteMembers lconv = | ||
| Set.fromList . map (tUntagged . rmId) . convRemoteMembers . tUnqualified $ | ||
| lconv | ||
|
|
||
| existingMembers :: Local Data.Conversation -> Set (Qualified UserId) | ||
| existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv | ||
|
|
||
| addMembers :: | ||
| HasProposalActionEffects r => | ||
| Qualified UserId -> | ||
| Maybe ConnId -> | ||
| Local ConvOrSubConv -> | ||
| NonEmpty (Qualified UserId) -> | ||
| Sem r [LocalConversationUpdate] | ||
| addMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of | ||
| Conv mlsConv -> do | ||
| let lconv = qualifyAs lconvOrSub (mcConv mlsConv) | ||
| -- FUTUREWORK: update key package ref mapping to reflect conversation membership | ||
| foldMap | ||
| ( handleNoChanges | ||
| . handleMLSProposalFailures @ProposalErrors | ||
| . fmap pure | ||
| . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con | ||
| . flip ConversationJoin roleNameWireMember | ||
| ) | ||
| . nonEmpty | ||
| . filter (flip Set.notMember (existingMembers lconv)) | ||
| . toList | ||
| $ users | ||
| SubConv _ _ -> pure [] | ||
|
|
||
| removeMembers :: | ||
| HasProposalActionEffects r => | ||
| Qualified UserId -> | ||
| Maybe ConnId -> | ||
| Local ConvOrSubConv -> | ||
| NonEmpty (Qualified UserId) -> | ||
| Sem r [LocalConversationUpdate] | ||
| removeMembers qusr con lconvOrSub users = case tUnqualified lconvOrSub of | ||
| Conv mlsConv -> do | ||
| let lconv = qualifyAs lconvOrSub (mcConv mlsConv) | ||
| foldMap | ||
| ( handleNoChanges | ||
| . handleMLSProposalFailures @ProposalErrors | ||
| . fmap pure | ||
| . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con | ||
| ) | ||
| . nonEmpty | ||
| . filter (flip Set.member (existingMembers lconv)) | ||
| . toList | ||
| $ users | ||
| SubConv _ _ -> pure [] | ||
|
|
||
| handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a | ||
| handleNoChanges = fmap fold . runError | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,8 +214,8 @@ tests s = | |
| [ test s "get subconversation of MLS conv - 200" (testCreateSubConv True), | ||
| test s "get subconversation of Proteus conv - 404" (testCreateSubConv False), | ||
| test s "join subconversation with an external commit bundle" testJoinSubConv, | ||
| test s "join subconversation with a client that is not in the main conv" testJoinSubNonMemberClient, | ||
| test s "add another client to a subconversation" testAddClientSubConv, | ||
| test s "join subconversation with a client that is not in the parent conv" testJoinSubNonMemberClient, | ||
| test s "fail to add another client to a subconversation via internal commit" testAddClientSubConv, | ||
| test s "remove another client from a subconversation" testRemoveClientSubConv, | ||
| test s "send an application message in a subconversation" testSendMessageSubConv, | ||
| test s "reset a subconversation as a member" (testDeleteSubConv True), | ||
|
|
@@ -2361,7 +2361,7 @@ testJoinSubConv = do | |
| sub <- | ||
| liftTest $ | ||
| responseJsonError | ||
| =<< getSubConv (qUnqualified bob) qcnv (SubConvId "conference") | ||
| =<< getSubConv (qUnqualified bob) qcnv subId | ||
| <!! const 200 === statusCode | ||
|
|
||
| resetGroup bob1 (pscGroupId sub) | ||
|
|
@@ -2381,13 +2381,63 @@ testJoinSubConv = do | |
| createExternalCommit alice1 Nothing (fmap (flip SubConv subId) qcnv) | ||
| >>= sendAndConsumeCommitBundle | ||
|
|
||
| -- FUTUREWORK: implement the following tests | ||
|
|
||
| testJoinSubNonMemberClient :: TestM () | ||
| testJoinSubNonMemberClient = pure () | ||
| testJoinSubNonMemberClient = do | ||
| [alice, bob] <- createAndConnectUsers [Nothing, Nothing] | ||
|
|
||
| runMLSTest $ do | ||
| [alice1, alice2, bob1] <- | ||
| traverse createMLSClient [alice, alice, bob] | ||
| traverse_ uploadNewKeyPackage [bob1, alice2] | ||
| (_, qcnv) <- setupMLSGroup alice1 | ||
| void $ createAddCommit alice1 [alice] >>= sendAndConsumeCommit | ||
|
|
||
| let subId = SubConvId "conference" | ||
| void $ createSubConv qcnv alice1 subId | ||
|
|
||
| -- now Bob attempts to get the group info so he can join via external commit | ||
| -- with his own client, but he cannot because he is not a member of the | ||
| -- parent conversation | ||
| getGroupInfo (ciUser bob1) (fmap (flip SubConv subId) qcnv) | ||
| !!! const 404 === statusCode | ||
|
|
||
| testAddClientSubConv :: TestM () | ||
| testAddClientSubConv = pure () | ||
| testAddClientSubConv = do | ||
| [alice, bob] <- createAndConnectUsers [Nothing, Nothing] | ||
| runMLSTest $ do | ||
| [alice1, bob1] <- traverse createMLSClient [alice, bob] | ||
| void $ uploadNewKeyPackage bob1 | ||
| (_, qcnv) <- setupMLSGroup alice1 | ||
| void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit | ||
|
|
||
| let subId = SubConvId "conference" | ||
| void $ createSubConv qcnv alice1 subId | ||
|
|
||
| void $ uploadNewKeyPackage bob1 | ||
|
|
||
| commit <- createAddCommit alice1 [bob] | ||
| (createBundle commit >>= postCommitBundle (mpSender commit)) | ||
| !!! do | ||
| const 400 === statusCode | ||
| const (Just "Add proposals in subconversations are not supported") | ||
| === fmap Wai.message . responseJsonError | ||
|
|
||
| finalSub <- | ||
| liftTest $ | ||
| responseJsonError | ||
| =<< getSubConv (qUnqualified alice) qcnv subId | ||
| <!! const 200 === statusCode | ||
| liftIO $ do | ||
| assertEqual | ||
| "The subconversation has Bob in it, while it shouldn't" | ||
| [alice1] | ||
| (pscMembers finalSub) | ||
| assertEqual | ||
| "The subconversation epoch has moved beyond 1" | ||
| (Epoch 1) | ||
| (pscEpoch finalSub) | ||
|
|
||
| -- FUTUREWORK: implement the following tests | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having these stub tests below feels ripe for both merge conflicts and the assumption of tests existing by their outputs being green instead of yellow or something like that,. |
||
|
|
||
| testRemoveClientSubConv :: TestM () | ||
| testRemoveClientSubConv = pure () | ||
|
|
@@ -2409,9 +2459,9 @@ testSendMessageSubConv = do | |
| (_, qcnv) <- setupMLSGroup alice1 | ||
| void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit | ||
|
|
||
| let subname = "conference" | ||
| void $ createSubConv qcnv bob1 subname | ||
| let qcs = convsub qcnv (Just subname) | ||
| let subId = SubConvId "conference" | ||
| void $ createSubConv qcnv bob1 subId | ||
| let qcs = convsub qcnv (Just subId) | ||
|
|
||
| void $ createExternalCommit alice1 Nothing qcs >>= sendAndConsumeCommitBundle | ||
| void $ createExternalCommit bob2 Nothing qcs >>= sendAndConsumeCommitBundle | ||
|
|
@@ -2602,7 +2652,7 @@ testDeleteSubConv isAMember = do | |
| (qcnv, sub) <- runMLSTest $ do | ||
| alice1 <- createMLSClient alice | ||
| (_, qcnv) <- setupMLSGroup alice1 | ||
| sub <- createSubConv qcnv alice1 (unSubConvId sconv) | ||
| sub <- createSubConv qcnv alice1 sconv | ||
| pure (qcnv, sub) | ||
|
|
||
| let dsc = DeleteSubConversation (pscGroupId sub) (pscEpoch sub) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd avoid adding these calls to the umbrella type 🤔 and instead only add them to the functions that need the constraint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cherry-picked this change from the PR you and Paolo have been working on; I suppose you changed this in the meantime so I'll update it here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. I recall the
on-mls-message-sent, andon-conversation-updatedbeing there, but I assumed those were always needed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three use sites of the
HasProposalActionEffectstype alias. If I removeCallsFed 'Galley "on-new-remote-conversation"from this type alias, all the three use sites are missing theCallsFedconstraint.