diff --git a/Makefile b/Makefile index ea55403761f..1cd6be92e1e 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ crm: c db-migrate # Usage: TEST_INCLUDE=test1,test2 make devtest .PHONY: devtest devtest: - ghcid --command 'cabal repl integration' --test='Testlib.Run.mainI []' + ghcid --command 'cabal repl lib:integration' --test='Testlib.Run.mainI []' .PHONY: sanitize-pr sanitize-pr: diff --git a/integration/default.nix b/integration/default.nix index 37d66c8daf5..a52a1d34169 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -22,6 +22,8 @@ , cookie , cql , cql-io +, criterion +, cryptobox-haskell , crypton , crypton-x509 , cryptostore @@ -119,6 +121,8 @@ mkDerivation { cookie cql cql-io + criterion + cryptobox-haskell crypton crypton-x509 cryptostore diff --git a/integration/integration.cabal b/integration/integration.cabal index a3989f28e76..fa293712177 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -217,6 +217,8 @@ library , cookie , cql , cql-io + , criterion + , cryptobox-haskell , crypton , crypton-x509 , cryptostore diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d1c4066ae70..71b148bab7d 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -120,17 +120,15 @@ deleteTeamMember tid owner mem = do putConversationProtocol :: ( HasCallStack, MakesValue user, - MakesValue qcnv, MakesValue protocol ) => user -> - qcnv -> + ConvId -> protocol -> App Response -putConversationProtocol user qcnv protocol = do - (domain, cnv) <- objQid qcnv +putConversationProtocol user convId protocol = do p <- asString protocol - req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", domain, cnv, "protocol"]) + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId.domain, convId.id_, "protocol"]) submit "PUT" (req & addJSONObject ["protocol" .= p]) getConversation :: @@ -148,21 +146,19 @@ getConversation user qcnv = do getSubConversation :: ( HasCallStack, - MakesValue user, - MakesValue conv + MakesValue user ) => user -> - conv -> + ConvId -> String -> App Response getSubConversation user conv sub = do - (cnvDomain, cnvId) <- objQid conv req <- baseRequest user Galley Versioned $ joinHttpPath [ "conversations", - cnvDomain, - cnvId, + conv.domain, + conv.id_, "subconversations", sub ] @@ -184,16 +180,15 @@ deleteSubConversation user sub = do submit "DELETE" $ req & addJSONObject ["group_id" .= groupId, "epoch" .= epoch] leaveSubConversation :: - (HasCallStack, MakesValue user, MakesValue sub) => + (HasCallStack, MakesValue user) => user -> - sub -> + ConvId -> App Response -leaveSubConversation user sub = do - (conv, Just subId) <- objSubConv sub - (domain, convId) <- objQid conv +leaveSubConversation user convId = do + let Just subId = convId.subconvId req <- baseRequest user Galley Versioned - $ joinHttpPath ["conversations", domain, convId, "subconversations", subId, "self"] + $ joinHttpPath ["conversations", convId.domain, convId.id_, "subconversations", subId, "self"] submit "DELETE" req getSelfConversation :: (HasCallStack, MakesValue user) => user -> App Response @@ -278,16 +273,14 @@ mkProteusRecipients dom userClients msg = do & #text .~ fromString msg getGroupInfo :: - (HasCallStack, MakesValue user, MakesValue conv) => + (HasCallStack, MakesValue user) => user -> - conv -> + ConvId -> App Response getGroupInfo user conv = do - (qcnv, mSub) <- objSubConv conv - (convDomain, convId) <- objQid qcnv - let path = joinHttpPath $ case mSub of - Nothing -> ["conversations", convDomain, convId, "groupinfo"] - Just sub -> ["conversations", convDomain, convId, "subconversations", sub, "groupinfo"] + let path = joinHttpPath $ case conv.subconvId of + Nothing -> ["conversations", conv.domain, conv.id_, "groupinfo"] + Just sub -> ["conversations", conv.domain, conv.id_, "subconversations", sub, "groupinfo"] req <- baseRequest user Galley Versioned path submit "GET" req @@ -323,7 +316,7 @@ deleteTeamConv :: App Response deleteTeamConv team conv user = do teamId <- objId team - convId <- objId conv + convId <- objId $ objQidObject conv req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId]) submit "DELETE" req diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index f5e753cf88b..ae3d748f04d 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -3,6 +3,7 @@ module MLS.Util where import API.Brig +import API.BrigCommon import API.Galley import Control.Concurrent.Async hiding (link) import Control.Monad @@ -11,7 +12,6 @@ import Control.Monad.Codensity import Control.Monad.Cont import Control.Monad.Reader import Control.Monad.Trans.Maybe -import qualified Data.Aeson as A import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as Base64 @@ -33,6 +33,7 @@ import System.Directory import System.Exit import System.FilePath import System.IO hiding (print, putStrLn) +import System.IO.Error (isAlreadyExistsError) import System.IO.Temp import System.Posix.Files import System.Process @@ -40,6 +41,7 @@ import Testlib.Assertions import Testlib.HTTP import Testlib.JSON import Testlib.Prelude +import Testlib.Printing mkClientIdentity :: (MakesValue u, MakesValue c) => u -> c -> App ClientIdentity mkClientIdentity u c = do @@ -52,18 +54,12 @@ cid2Str cid = cid.user <> ":" <> cid.client <> "@" <> cid.domain data MessagePackage = MessagePackage { sender :: ClientIdentity, + convId :: ConvId, message :: ByteString, welcome :: Maybe ByteString, groupInfo :: Maybe ByteString } -getConv :: App Value -getConv = do - mls <- getMLSState - case mls.convId of - Nothing -> assertFailure "Uninitialised test conversation" - Just convId -> pure convId - toRandomFile :: ByteString -> App FilePath toRandomFile bs = do p <- randomFileName @@ -75,16 +71,15 @@ randomFileName = do bd <- getBaseDir (bd ) . UUID.toString <$> liftIO UUIDV4.nextRandom -mlscli :: (HasCallStack) => ClientIdentity -> [String] -> Maybe ByteString -> App ByteString -mlscli cid args mbstdin = do +mlscli :: (HasCallStack) => Maybe ConvId -> Ciphersuite -> ClientIdentity -> [String] -> Maybe ByteString -> App ByteString +mlscli mConvId cs cid args mbstdin = do groupOut <- randomFileName let substOut = argSubst "" groupOut - cs <- (.ciphersuite) <$> getMLSState let scheme = csSignatureScheme cs gs <- getClientGroupState cid - substIn <- case gs.group of + substIn <- case flip Map.lookup gs.groups =<< mConvId of Nothing -> pure id Just groupData -> do fn <- toRandomFile groupData @@ -92,7 +87,11 @@ mlscli cid args mbstdin = do store <- case Map.lookup scheme gs.keystore of Nothing -> do bd <- getBaseDir - liftIO $ createDirectory (bd cid2Str cid) + liftIO (createDirectory (bd cid2Str cid)) + `catch` \e -> + if (isAlreadyExistsError e) + then assertFailure "client directory for mls state already exists" + else throwM e -- initialise new keystore path <- randomFileName @@ -109,11 +108,15 @@ mlscli cid args mbstdin = do out <- runCli store args' mbstdin setGroup <- do groupOutWritten <- liftIO $ doesFileExist groupOut - if groupOutWritten - then do + case (groupOutWritten, mConvId) of + (True, Just convId) -> do groupData <- liftIO (BS.readFile groupOut) - pure $ \x -> x {group = Just groupData} - else pure id + pure $ \x -> x {groups = Map.insert convId groupData x.groups} + (True, Nothing) -> do + print $ colored red "mls-test-cli: Group was written but no convId was provided, this probably indicates something is going to go wrong in this test." + print =<< liftIO (prettierCallStack callStack) + pure id + _ -> pure id setStore <- do storeData <- liftIO (BS.readFile store) pure $ \x -> x {keystore = Map.insert scheme storeData x.keystore} @@ -137,27 +140,28 @@ argSubst :: String -> String -> String -> String argSubst from to_ s = if s == from then to_ else s -createWireClient :: (MakesValue u, HasCallStack) => u -> App ClientIdentity -createWireClient u = do - addClient u def +createWireClient :: (MakesValue u, HasCallStack) => u -> AddClient -> App ClientIdentity +createWireClient u clientArgs = do + addClient u clientArgs >>= getJSON 201 >>= mkClientIdentity u data InitMLSClient = InitMLSClient - {credType :: CredentialType} + { credType :: CredentialType, + clientArgs :: AddClient + } instance Default InitMLSClient where - def = InitMLSClient {credType = BasicCredentialType} + def = InitMLSClient {credType = BasicCredentialType, clientArgs = def} -- | Create new mls client and register with backend. -createMLSClient :: (MakesValue u, HasCallStack) => InitMLSClient -> u -> App ClientIdentity -createMLSClient opts u = do - cid <- createWireClient u +createMLSClient :: (MakesValue u, HasCallStack) => Ciphersuite -> InitMLSClient -> u -> App ClientIdentity +createMLSClient ciphersuite opts u = do + cid <- createWireClient u opts.clientArgs setClientGroupState cid def {credType = opts.credType} -- set public key - pkey <- mlscli cid ["public-key"] Nothing - ciphersuite <- (.ciphersuite) <$> getMLSState + pkey <- mlscli Nothing ciphersuite cid ["public-key"] Nothing bindResponse ( updateClient cid @@ -170,9 +174,9 @@ createMLSClient opts u = do pure cid -- | create and upload to backend -uploadNewKeyPackage :: (HasCallStack) => ClientIdentity -> App String -uploadNewKeyPackage cid = do - (kp, ref) <- generateKeyPackage cid +uploadNewKeyPackage :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App String +uploadNewKeyPackage suite cid = do + (kp, ref) <- generateKeyPackage cid suite -- upload key package bindResponse (uploadKeyPackages cid [kp]) $ \resp -> @@ -180,94 +184,100 @@ uploadNewKeyPackage cid = do pure ref -generateKeyPackage :: (HasCallStack) => ClientIdentity -> App (ByteString, String) -generateKeyPackage cid = do - suite <- (.ciphersuite) <$> getMLSState - kp <- mlscli cid ["key-package", "create", "--ciphersuite", suite.code] Nothing - ref <- B8.unpack . Base64.encode <$> mlscli cid ["key-package", "ref", "-"] (Just kp) +generateKeyPackage :: (HasCallStack) => ClientIdentity -> Ciphersuite -> App (ByteString, String) +generateKeyPackage cid suite = do + kp <- mlscli Nothing suite cid ["key-package", "create", "--ciphersuite", suite.code] Nothing + ref <- B8.unpack . Base64.encode <$> mlscli Nothing suite cid ["key-package", "ref", "-"] (Just kp) fp <- keyPackageFile cid ref liftIO $ BS.writeFile fp kp pure (kp, ref) -- | Create conversation and corresponding group. -createNewGroup :: (HasCallStack) => ClientIdentity -> App (String, Value) -createNewGroup cid = do +createNewGroup :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App ConvId +createNewGroup cs cid = do conv <- postConversation cid defMLS >>= getJSON 201 - groupId <- conv %. "group_id" & asString - convId <- conv %. "qualified_id" - createGroup cid conv - pure (groupId, convId) + convId <- objConvId conv + createGroup cs cid convId + pure convId -- | Retrieve self conversation and create the corresponding group. -createSelfGroup :: (HasCallStack) => ClientIdentity -> App (String, Value) -createSelfGroup cid = do +createSelfGroup :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App (String, Value) +createSelfGroup cs cid = do conv <- getSelfConversation cid >>= getJSON 200 + convId <- objConvId conv groupId <- conv %. "group_id" & asString - createGroup cid conv + createGroup cs cid convId pure (groupId, conv) -createGroup :: (MakesValue conv) => ClientIdentity -> conv -> App () -createGroup cid conv = do - mls <- getMLSState - case mls.groupId of - Just _ -> assertFailure "only one group can be created" - Nothing -> pure () - resetGroup cid conv - -createSubConv :: (HasCallStack) => ClientIdentity -> String -> App () -createSubConv cid subId = do - mls <- getMLSState - sub <- getSubConversation cid mls.convId subId >>= getJSON 200 - resetGroup cid sub - void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle - -createOne2OneSubConv :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () -createOne2OneSubConv cid subId keys = do - mls <- getMLSState - sub <- getSubConversation cid mls.convId subId >>= getJSON 200 - resetOne2OneGroupGeneric cid sub keys - void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle - -resetGroup :: (HasCallStack, MakesValue conv) => ClientIdentity -> conv -> App () -resetGroup cid conv = do - convId <- objSubConvObject conv - groupId <- conv %. "group_id" & asString +createGroup :: Ciphersuite -> ClientIdentity -> ConvId -> App () +createGroup cs cid convId = do + let Just groupId = convId.groupId modifyMLSState $ \s -> - s - { groupId = Just groupId, - convId = Just convId, - members = Set.singleton cid, - epoch = 0, - newMembers = mempty - } + let mlsConv = + MLSConv + { members = Set.singleton cid, + newMembers = mempty, + groupId, + convId = convId, + epoch = 0, + ciphersuite = cs + } + in s {convs = Map.insert convId mlsConv s.convs} keys <- getMLSPublicKeys cid.qualifiedUserId >>= getJSON 200 - resetClientGroup cid groupId keys - -resetOne2OneGroup :: (HasCallStack, MakesValue one2OneConv) => ClientIdentity -> one2OneConv -> App () -resetOne2OneGroup cid one2OneConv = - resetOne2OneGroupGeneric cid (one2OneConv %. "conversation") (one2OneConv %. "public_keys") + resetClientGroup cs cid groupId convId keys + +createSubConv :: (HasCallStack) => Ciphersuite -> ConvId -> ClientIdentity -> String -> App () +createSubConv cs convId cid subId = do + sub <- getSubConversation cid convId subId >>= getJSON 200 + subConvId <- objConvId sub + createGroup cs cid subConvId + void $ createPendingProposalCommit subConvId cid >>= sendAndConsumeCommitBundle + +createOne2OneSubConv :: (HasCallStack, MakesValue keys) => Ciphersuite -> ConvId -> ClientIdentity -> String -> keys -> App () +createOne2OneSubConv cs convId cid subId keys = do + sub <- getSubConversation cid convId subId >>= getJSON 200 + subConvId <- objConvId sub + resetOne2OneGroupGeneric cs cid sub keys + void $ createPendingProposalCommit subConvId cid >>= sendAndConsumeCommitBundle + +resetOne2OneGroup :: (HasCallStack, MakesValue one2OneConv) => Ciphersuite -> ClientIdentity -> one2OneConv -> App () +resetOne2OneGroup cs cid one2OneConv = + resetOne2OneGroupGeneric cs cid (one2OneConv %. "conversation") (one2OneConv %. "public_keys") -- | Useful when keys are to be taken from main conv and the conv here is the subconv -resetOne2OneGroupGeneric :: (HasCallStack, MakesValue conv, MakesValue keys) => ClientIdentity -> conv -> keys -> App () -resetOne2OneGroupGeneric cid conv keys = do - convId <- objSubConvObject conv +resetOne2OneGroupGeneric :: (HasCallStack, MakesValue conv, MakesValue keys) => Ciphersuite -> ClientIdentity -> conv -> keys -> App () +resetOne2OneGroupGeneric cs cid conv keys = do + convId <- objConvId conv groupId <- conv %. "group_id" & asString modifyMLSState $ \s -> - s - { groupId = Just groupId, - convId = Just convId, - members = Set.singleton cid, - epoch = 0, - newMembers = mempty - } - resetClientGroup cid groupId keys - -resetClientGroup :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () -resetClientGroup cid gid keys = do - mls <- getMLSState - removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme mls.ciphersuite) + let newMLSConv = + MLSConv + { members = Set.singleton cid, + newMembers = mempty, + groupId = groupId, + convId = convId, + epoch = 0, + ciphersuite = cs + } + resetConv old new = + old + { groupId = new.groupId, + convId = new.convId, + members = new.members, + newMembers = new.newMembers, + epoch = new.epoch + } + in s {convs = Map.insertWith resetConv convId newMLSConv s.convs} + + resetClientGroup cs cid groupId convId keys + +resetClientGroup :: (HasCallStack, MakesValue keys) => Ciphersuite -> ClientIdentity -> String -> ConvId -> keys -> App () +resetClientGroup cs cid gid convId keys = do + removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme cs) void $ mlscli + (Just convId) + cs cid [ "group", "create", @@ -276,7 +286,7 @@ resetClientGroup cid gid keys = do "--group-out", "", "--ciphersuite", - mls.ciphersuite.code, + cs.code, gid ] (Just removalKey) @@ -310,13 +320,13 @@ unbundleKeyPackages bundle = do -- Note that this alters the state of the group immediately. If we want to test -- a scenario where the commit is rejected by the backend, we can restore the -- group to the previous state by using an older version of the group file. -createAddCommit :: (HasCallStack) => ClientIdentity -> [Value] -> App MessagePackage -createAddCommit cid users = do - mls <- getMLSState +createAddCommit :: (HasCallStack) => ClientIdentity -> ConvId -> [Value] -> App MessagePackage +createAddCommit cid convId users = do + conv <- getMLSConv convId kps <- fmap concat . for users $ \user -> do - bundle <- claimKeyPackages mls.ciphersuite cid user >>= getJSON 200 + bundle <- claimKeyPackages conv.ciphersuite cid user >>= getJSON 200 unbundleKeyPackages bundle - createAddCommitWithKeyPackages cid kps + createAddCommitWithKeyPackages cid convId kps withTempKeyPackageFile :: ByteString -> ContT a App FilePath withTempKeyPackageFile bs = do @@ -332,15 +342,19 @@ withTempKeyPackageFile bs = do createAddCommitWithKeyPackages :: (HasCallStack) => ClientIdentity -> + ConvId -> [(ClientIdentity, ByteString)] -> App MessagePackage -createAddCommitWithKeyPackages cid clientsAndKeyPackages = do +createAddCommitWithKeyPackages cid convId clientsAndKeyPackages = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" giFile <- liftIO $ emptyTempFile bd "gi" + Just conv <- Map.lookup convId . (.convs) <$> getMLSState commit <- runContT (traverse (withTempKeyPackageFile . snd) clientsAndKeyPackages) $ \kpFiles -> mlscli + (Just convId) + conv.ciphersuite cid ( [ "member", "add", @@ -359,7 +373,13 @@ createAddCommitWithKeyPackages cid clientsAndKeyPackages = do modifyMLSState $ \mls -> mls - { newMembers = Set.fromList (map fst clientsAndKeyPackages) + { convs = + Map.adjust + ( \oldConvState -> + oldConvState {newMembers = Set.fromList (map fst clientsAndKeyPackages)} + ) + convId + mls.convs } welcome <- liftIO $ BS.readFile welcomeFile @@ -367,25 +387,30 @@ createAddCommitWithKeyPackages cid clientsAndKeyPackages = do pure $ MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Just welcome, groupInfo = Just gi } -createRemoveCommit :: (HasCallStack) => ClientIdentity -> [ClientIdentity] -> App MessagePackage -createRemoveCommit cid targets = do +createRemoveCommit :: (HasCallStack) => ClientIdentity -> ConvId -> [ClientIdentity] -> App MessagePackage +createRemoveCommit cid convId targets = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" giFile <- liftIO $ emptyTempFile bd "gi" groupStateMap <- do gs <- getClientGroupState cid - groupData <- assertJust "Group state not initialised" gs.group + groupData <- assertJust "Group state not initialised" (Map.lookup convId gs.groups) Map.fromList <$> readGroupState groupData let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets + conv <- getMLSConv convId + commit <- mlscli + (Just convId) + conv.ciphersuite cid ( [ "member", "remove", @@ -408,58 +433,71 @@ createRemoveCommit cid targets = do pure MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Just welcome, groupInfo = Just gi } -createAddProposals :: (HasCallStack) => ClientIdentity -> [Value] -> App [MessagePackage] -createAddProposals cid users = do - mls <- getMLSState +createAddProposals :: (HasCallStack) => ConvId -> ClientIdentity -> [Value] -> App [MessagePackage] +createAddProposals convId cid users = do + Just mls <- Map.lookup convId . (.convs) <$> getMLSState bundles <- for users $ (claimKeyPackages mls.ciphersuite cid >=> getJSON 200) kps <- concat <$> traverse unbundleKeyPackages bundles - traverse (createAddProposalWithKeyPackage cid) kps + traverse (createAddProposalWithKeyPackage convId cid) kps -createReInitProposal :: (HasCallStack) => ClientIdentity -> App MessagePackage -createReInitProposal cid = do +createReInitProposal :: (HasCallStack) => ConvId -> ClientIdentity -> App MessagePackage +createReInitProposal convId cid = do + conv <- getMLSConv convId prop <- mlscli + (Just convId) + conv.ciphersuite cid ["proposal", "--group-in", "", "--group-out", "", "re-init"] Nothing pure MessagePackage { sender = cid, + convId = convId, message = prop, welcome = Nothing, groupInfo = Nothing } createAddProposalWithKeyPackage :: + ConvId -> ClientIdentity -> (ClientIdentity, ByteString) -> App MessagePackage -createAddProposalWithKeyPackage cid (_, kp) = do +createAddProposalWithKeyPackage convId cid (_, kp) = do + conv <- getMLSConv convId prop <- runContT (withTempKeyPackageFile kp) $ \kpFile -> mlscli + (Just convId) + conv.ciphersuite cid ["proposal", "--group-in", "", "--group-out", "", "add", kpFile] Nothing pure MessagePackage { sender = cid, + convId = convId, message = prop, welcome = Nothing, groupInfo = Nothing } -createPendingProposalCommit :: (HasCallStack) => ClientIdentity -> App MessagePackage -createPendingProposalCommit cid = do +createPendingProposalCommit :: (HasCallStack) => ConvId -> ClientIdentity -> App MessagePackage +createPendingProposalCommit convId cid = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" pgsFile <- liftIO $ emptyTempFile bd "pgs" + conv <- getMLSConv convId commit <- mlscli + (Just convId) + conv.ciphersuite cid [ "commit", "--group", @@ -478,6 +516,7 @@ createPendingProposalCommit cid = do pure MessagePackage { sender = cid, + convId = convId, message = commit, welcome = welcome, groupInfo = Just pgs @@ -485,18 +524,21 @@ createPendingProposalCommit cid = do createExternalCommit :: (HasCallStack) => + ConvId -> ClientIdentity -> Maybe ByteString -> App MessagePackage -createExternalCommit cid mgi = do +createExternalCommit convId cid mgi = do bd <- getBaseDir giFile <- liftIO $ emptyTempFile bd "gi" - conv <- getConv gi <- case mgi of - Nothing -> getGroupInfo cid conv >>= getBody 200 + Nothing -> getGroupInfo cid convId >>= getBody 200 Just v -> pure v + conv <- getMLSConv convId commit <- mlscli + (Just convId) + conv.ciphersuite cid [ "external-commit", "--group-info-in", @@ -510,7 +552,7 @@ createExternalCommit cid mgi = do modifyMLSState $ \mls -> mls - { newMembers = Set.singleton cid + { convs = Map.adjust (\oldConvState -> oldConvState {newMembers = Set.singleton cid}) convId mls.convs -- This might be a different client than those that have been in the -- group from before. } @@ -519,6 +561,7 @@ createExternalCommit cid mgi = do pure $ MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Nothing, groupInfo = Just newPgs @@ -527,25 +570,13 @@ createExternalCommit cid mgi = do data MLSNotificationTag = MLSNotificationMessageTag | MLSNotificationWelcomeTag deriving (Show, Eq, Ord) --- | Extract a conversation ID (including an optional subconversation) from an --- event object. -eventSubConv :: (HasCallStack) => (MakesValue event) => event -> App Value -eventSubConv event = do - sub <- lookupField event "subconv" - conv <- event %. "qualified_conversation" - objSubConvObject $ - object - [ "parent_qualified_id" .= conv, - "subconv_id" .= sub - ] - -consumingMessages :: (HasCallStack) => MessagePackage -> Codensity App () -consumingMessages mp = Codensity $ \k -> do - mls <- getMLSState +consumingMessages :: (HasCallStack) => MLSProtocol -> MessagePackage -> Codensity App () +consumingMessages mlsProtocol mp = Codensity $ \k -> do + conv <- getMLSConv mp.convId -- clients that should receive the message itself - let oldClients = Set.delete mp.sender mls.members + let oldClients = Set.delete mp.sender conv.members -- clients that should receive a welcome message - let newClients = Set.delete mp.sender mls.newMembers + let newClients = Set.delete mp.sender conv.newMembers -- all clients that should receive some MLS notification, together with the -- expected notification tag let clients = @@ -561,10 +592,12 @@ consumingMessages mp = Codensity $ \k -> do r <- k () -- if the conversation is actually MLS (and not mixed), pick one client for - -- each new user and wait for its join event - when (mls.protocol == MLSProtocolMLS) $ + -- each new user and wait for its join event. In Mixed protocol, the user is + -- already in the conversation so they do not get a member-join + -- notification. + when (mlsProtocol == MLSProtocolMLS) $ traverse_ - (awaitMatch isMemberJoinNotif) + (awaitMatch (\n -> isMemberJoinNotif n)) ( flip Map.restrictKeys newUsers . Map.mapKeys ((.user) . fst) . Map.fromList @@ -575,50 +608,53 @@ consumingMessages mp = Codensity $ \k -> do -- at this point we know that every new user has been added to the -- conversation for_ (zip clients wss) $ \((cid, t), ws) -> case t of - MLSNotificationMessageTag -> void $ consumeMessageNoExternal cid (Just mp) ws + MLSNotificationMessageTag -> void $ consumeMessageNoExternal conv.ciphersuite cid mp ws MLSNotificationWelcomeTag -> consumeWelcome cid mp ws pure r -consumeMessageWithPredicate :: (HasCallStack) => (Value -> App Bool) -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value -consumeMessageWithPredicate p cid mmp ws = do - mls <- getMLSState +consumeMessageWithPredicate :: (HasCallStack) => (Value -> App Bool) -> ConvId -> Ciphersuite -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value +consumeMessageWithPredicate p convId cs cid mmp ws = do notif <- awaitMatch p ws event <- notif %. "payload.0" + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId + lookupField event "subconv" `shouldMatch` convId.subconvId + for_ mmp $ \mp -> do - shouldMatch (eventSubConv event) (fromMaybe A.Null mls.convId) - shouldMatch (event %. "from") mp.sender.user - shouldMatch (event %. "data") (B8.unpack (Base64.encode mp.message)) + event %. "from" `shouldMatch` mp.sender.user + event %. "data" `shouldMatch` (B8.unpack (Base64.encode mp.message)) msgData <- event %. "data" & asByteString - _ <- mlsCliConsume cid msgData - showMessage cid msgData + _ <- mlsCliConsume convId cs cid msgData + showMessage cs cid msgData -- | Get a single MLS message from a websocket and consume it. Return a JSON -- representation of the message. -consumeMessage :: (HasCallStack) => ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value +consumeMessage :: (HasCallStack) => ConvId -> Ciphersuite -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value consumeMessage = consumeMessageWithPredicate isNewMLSMessageNotif -- | like 'consumeMessage' but will not consume a message where the sender is the backend -consumeMessageNoExternal :: (HasCallStack) => ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value -consumeMessageNoExternal cid = consumeMessageWithPredicate isNewMLSMessageNotifButNoProposal cid +consumeMessageNoExternal :: (HasCallStack) => Ciphersuite -> ClientIdentity -> MessagePackage -> WebSocket -> App Value +consumeMessageNoExternal cs cid mp = consumeMessageWithPredicate isNewMLSMessageNotifButNoProposal mp.convId cs cid (Just mp) where -- the backend (correctly) reacts to a commit removing someone from a parent conversation with a -- remove proposal, however, we don't want to consume this here isNewMLSMessageNotifButNoProposal :: Value -> App Bool isNewMLSMessageNotifButNoProposal n = do - isNotif <- isNewMLSMessageNotif n - if isNotif + isRelevantNotif <- isNewMLSMessageNotif n &&~ isNotifConvId mp.convId n + if isRelevantNotif then do - msg <- n %. "payload.0.data" & asByteString >>= showMessage cid + msg <- n %. "payload.0.data" & asByteString >>= showMessage cs cid sender <- msg `lookupField` "message.content.sender" `catch` \(_ :: AssertionFailure) -> pure Nothing let backendSender = object ["External" .= Number 0] pure $ sender /= Just backendSender else pure False -mlsCliConsume :: (HasCallStack) => ClientIdentity -> ByteString -> App ByteString -mlsCliConsume cid msgData = +mlsCliConsume :: (HasCallStack) => ConvId -> Ciphersuite -> ClientIdentity -> ByteString -> App ByteString +mlsCliConsume convId cs cid msgData = mlscli + (Just convId) + cs cid [ "consume", "--group", @@ -632,58 +668,74 @@ mlsCliConsume cid msgData = -- | Send an MLS message, wait for clients to receive it, then consume it on -- the client side. If the message is a commit, the -- 'sendAndConsumeCommitBundle' function should be used instead. +-- +-- returns response body of 'postMLSMessage' sendAndConsumeMessage :: (HasCallStack) => MessagePackage -> App Value sendAndConsumeMessage mp = lowerCodensity $ do - consumingMessages mp + consumingMessages MLSProtocolMLS mp lift $ postMLSMessage mp.sender mp.message >>= getJSON 201 +sendAndConsumeCommitBundle :: (HasCallStack) => MessagePackage -> App Value +sendAndConsumeCommitBundle = sendAndConsumeCommitBundleWithProtocol MLSProtocolMLS + -- | Send an MLS commit bundle, wait for clients to receive it, consume it, and -- update the test state accordingly. -sendAndConsumeCommitBundle :: (HasCallStack) => MessagePackage -> App Value -sendAndConsumeCommitBundle mp = do +sendAndConsumeCommitBundleWithProtocol :: (HasCallStack) => MLSProtocol -> MessagePackage -> App Value +sendAndConsumeCommitBundleWithProtocol protocol mp = do lowerCodensity $ do - consumingMessages mp + consumingMessages protocol mp lift $ do r <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 -- if the sender is a new member (i.e. it's an external commit), then -- process the welcome message directly do - mls <- getMLSState - when (Set.member mp.sender mls.newMembers) $ - traverse_ (fromWelcome mp.sender) mp.welcome + conv <- getMLSConv mp.convId + when (Set.member mp.sender conv.newMembers) $ + traverse_ (fromWelcome mp.convId conv.ciphersuite mp.sender) mp.welcome -- increment epoch and add new clients modifyMLSState $ \mls -> mls - { epoch = epoch mls + 1, - members = members mls <> newMembers mls, - newMembers = mempty + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = conv.members <> conv.newMembers, + newMembers = mempty + } + ) + mp.convId + mls.convs } pure r consumeWelcome :: (HasCallStack) => ClientIdentity -> MessagePackage -> WebSocket -> App () consumeWelcome cid mp ws = do - mls <- getMLSState notif <- awaitMatch isWelcomeNotif ws event <- notif %. "payload.0" - shouldMatch (eventSubConv event) (fromMaybe A.Null mls.convId) - shouldMatch (event %. "from") mp.sender.user - shouldMatch (event %. "data") (fmap (B8.unpack . Base64.encode) mp.welcome) + event %. "qualified_conversation" `shouldMatch` convIdToQidObject mp.convId + lookupField event "subconv" `shouldMatch` mp.convId.subconvId + event %. "from" `shouldMatch` mp.sender.user + event %. "data" `shouldMatch` (fmap (B8.unpack . Base64.encode) mp.welcome) welcome <- event %. "data" & asByteString gs <- getClientGroupState cid assertBool "Existing clients in a conversation should not consume welcomes" - (isNothing gs.group) - fromWelcome cid welcome + (not $ Map.member mp.convId gs.groups) + conv <- getMLSConv mp.convId + fromWelcome mp.convId conv.ciphersuite cid welcome -fromWelcome :: ClientIdentity -> ByteString -> App () -fromWelcome cid welcome = +fromWelcome :: ConvId -> Ciphersuite -> ClientIdentity -> ByteString -> App () +fromWelcome convId cs cid welcome = void $ mlscli + (Just convId) + cs cid [ "group", "from-welcome", @@ -733,9 +785,9 @@ setClientGroupState cid g = modifyMLSState $ \s -> s {clientGroupState = Map.insert cid g (clientGroupState s)} -showMessage :: (HasCallStack) => ClientIdentity -> ByteString -> App Value -showMessage cid msg = do - bs <- mlscli cid ["show", "message", "-"] (Just msg) +showMessage :: (HasCallStack) => Ciphersuite -> ClientIdentity -> ByteString -> App Value +showMessage cs cid msg = do + bs <- mlscli Nothing cs cid ["show", "message", "-"] (Just msg) assertOne (Aeson.decode (BS.fromStrict bs)) readGroupState :: (HasCallStack) => ByteString -> App [(ClientIdentity, Word32)] @@ -760,12 +812,16 @@ readGroupState gs = do createApplicationMessage :: (HasCallStack) => + ConvId -> ClientIdentity -> String -> App MessagePackage -createApplicationMessage cid messageContent = do +createApplicationMessage convId cid messageContent = do + conv <- getMLSConv convId message <- mlscli + (Just convId) + conv.ciphersuite cid ["message", "--group-in", "", messageContent, "--group-out", ""] Nothing @@ -773,36 +829,40 @@ createApplicationMessage cid messageContent = do pure MessagePackage { sender = cid, + convId = convId, message = message, welcome = Nothing, groupInfo = Nothing } -setMLSCiphersuite :: Ciphersuite -> App () -setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} +setMLSCiphersuite :: ConvId -> Ciphersuite -> App () +setMLSCiphersuite convId suite = modifyMLSState $ \mls -> mls {convs = Map.adjust (\conv -> conv {ciphersuite = suite}) convId mls.convs} -leaveCurrentConv :: +leaveConv :: (HasCallStack) => + ConvId -> ClientIdentity -> App () -leaveCurrentConv cid = do - mls <- getMLSState - (_, mSubId) <- objSubConv mls.convId - case mSubId of +leaveConv convId cid = do + case convId.subconvId of -- FUTUREWORK: implement leaving main conversation as well Nothing -> assertFailure "Leaving conversations is not supported" Just _ -> do - void $ leaveSubConversation cid mls.convId >>= getBody 200 + void $ leaveSubConversation cid convId >>= getBody 200 modifyMLSState $ \s -> s - { members = Set.difference mls.members (Set.singleton cid) + { convs = Map.adjust (\conv -> conv {members = Set.delete cid conv.members}) convId s.convs } -getCurrentConv :: (HasCallStack) => ClientIdentity -> App Value -getCurrentConv cid = do - mls <- getMLSState - (conv, mSubId) <- objSubConv mls.convId - resp <- case mSubId of - Nothing -> getConversation cid conv - Just sub -> getSubConversation cid conv sub +getConv :: (HasCallStack) => ConvId -> ClientIdentity -> App Value +getConv convId cid = do + resp <- case convId.subconvId of + Nothing -> getConversation cid (convIdToQidObject convId) + Just sub -> getSubConversation cid convId sub getJSON 200 resp + +getSubConvId :: (MakesValue user, HasCallStack) => user -> ConvId -> String -> App ConvId +getSubConvId user convId subConvName = + getSubConversation user convId subConvName + >>= getJSON 200 + >>= objConvId diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index d99b46b8897..7c99e254d11 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -127,6 +127,12 @@ isConvLeaveNotifWithLeaver user n = isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) +isNotifConvId :: (MakesValue a, HasCallStack) => ConvId -> a -> App Bool +isNotifConvId conv n = do + let subconvField = "payload.0.subconv" + fieldEquals n "payload.0.qualified_conversation" (convIdToQidObject conv) + &&~ maybe (isNothing <$> lookupField n subconvField) (fieldEquals n subconvField) conv.subconvId + isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 4e19ae9b0a6..2cbd72c8ac0 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -28,6 +28,7 @@ import Data.Vector (fromList) import GHC.Stack import Testlib.MockIntegrationService (mkLegalHoldSettings) import Testlib.Prelude +import UnliftIO (pooledForConcurrentlyN) randomUser :: (HasCallStack, MakesValue domain) => domain -> CreateUser -> App Value randomUser domain cu = bindResponse (createUser domain cu) $ \resp -> do @@ -43,7 +44,7 @@ createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, createTeam domain memberCount = do owner <- createUser domain def {team = True} >>= getJSON 201 tid <- owner %. "team" & asString - members <- for [2 .. memberCount] $ \_ -> createTeamMember owner def + members <- pooledForConcurrentlyN 64 [2 .. memberCount] $ \_ -> createTeamMember owner def pure (owner, tid, members) data CreateTeamMember = CreateTeamMember @@ -131,7 +132,7 @@ getAllConvs u = do simpleMixedConversationSetup :: (HasCallStack, MakesValue domain) => domain -> - App (Value, Value, Value) + App (Value, Value, ConvId) simpleMixedConversationSetup secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def @@ -140,15 +141,17 @@ simpleMixedConversationSetup secondDomain = do conv <- postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} >>= getJSON 201 + >>= objConvId bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - conv' <- getConversation alice conv >>= getJSON 200 + convId <- + getConversation alice (convIdToQidObject conv) + >>= getJSON 200 + >>= objConvId - pure (alice, bob, conv') + pure (alice, bob, convId) supportMLS :: (HasCallStack, MakesValue u) => u -> App () supportMLS u = do @@ -403,3 +406,11 @@ uploadDownloadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (St uploadDownloadProfilePicture usr = do (dom, key, _payload) <- uploadProfilePicture usr downloadProfilePicture usr dom key + +addUsersToFailureContext :: (MakesValue user) => [(String, user)] -> App a -> App a +addUsersToFailureContext namesAndUsers action = do + let mkLine (name, user) = do + (domain, id_) <- objQid user + pure $ name <> ": " <> id_ <> "@" <> domain + allLines <- unlines <$> (mapM mkLine namesAndUsers) + addFailureContext allLines action diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index 01113946788..14c921fefe7 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -78,16 +78,17 @@ testAccessUpdateGuestRemoved proto = do >>= getJSON 201 pure (conv, clients) ConversationProtocolMLS -> do - alice1 <- createMLSClient def alice - clients <- traverse (createMLSClient def) [bob, charlie, dee] - traverse_ uploadNewKeyPackage clients + alice1 <- createMLSClient def def alice + clients <- traverse (createMLSClient def def) [bob, charlie, dee] + traverse_ (uploadNewKeyPackage def) clients conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 - createGroup alice1 conv + convId <- objConvId conv + createGroup def alice1 convId - void $ createAddCommit alice1 [bob, charlie, dee] >>= sendAndConsumeCommitBundle - convId <- conv %. "qualified_id" - pure (convId, map (.client) (alice1 : clients)) + void $ createAddCommit alice1 convId [bob, charlie, dee] >>= sendAndConsumeCommitBundle + convQid <- conv %. "qualified_id" + pure (convQid, map (.client) (alice1 : clients)) let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] void $ updateAccess alice conv update >>= getJSON 200 diff --git a/integration/test/Test/ExternalPartner.hs b/integration/test/Test/ExternalPartner.hs index 01bdd629834..4c9f748e564 100644 --- a/integration/test/Test/ExternalPartner.hs +++ b/integration/test/Test/ExternalPartner.hs @@ -60,7 +60,7 @@ testExternalPartnerPermissionsMls = do -- external partners should not be able to create (MLS) conversations (owner, _, _) <- createTeam OwnDomain 2 bobExt <- createTeamMember owner def {role = "partner"} - bobExtClient <- createMLSClient def bobExt + bobExtClient <- createMLSClient def def bobExt bindResponse (postConversation bobExtClient defMLS) $ \resp -> do resp.status `shouldMatchInt` 403 diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs index 55743ec4f91..45f099aef5c 100644 --- a/integration/test/Test/FeatureFlags/LegalHold.hs +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -111,7 +111,8 @@ testExposeInvitationURLsToTeamAdminConfig = do runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do let domain = testBackend.berDomain - let testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do + testNoAllowlistEntry :: (HasCallStack) => App (Value, String) + testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do (owner, tid, _) <- createTeam domain 1 checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index e8cc0b22743..7b0e3680aeb 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -914,9 +914,9 @@ testBlockLHForMLSUsers = do -- scenario 1: -- if charlie is in any MLS conversation, he cannot approve to be put under legalhold (charlie, tid, []) <- createTeam OwnDomain 1 - [charlie1] <- traverse (createMLSClient def) [charlie] - void $ createNewGroup charlie1 - void $ createAddCommit charlie1 [charlie] >>= sendAndConsumeCommitBundle + [charlie1] <- traverse (createMLSClient def def) [charlie] + convId <- createNewGroup def charlie1 + void $ createAddCommit charlie1 convId [charlie] >>= sendAndConsumeCommitBundle legalholdWhitelistTeam tid charlie >>= assertStatus 200 withMockServer def lhMockApp \lhDomAndPort _chan -> do @@ -934,9 +934,9 @@ testBlockLHForMLSUsers = do testBlockClaimingKeyPackageForLHUsers :: (HasCallStack) => App () testBlockClaimingKeyPackageForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 - [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] - _ <- uploadNewKeyPackage charlie1 - _ <- createNewGroup alice1 + [alice1, charlie1] <- traverse (createMLSClient def def) [alice, charlie] + _ <- uploadNewKeyPackage def charlie1 + _ <- createNewGroup def alice1 legalholdWhitelistTeam tid alice >>= assertStatus 200 withMockServer def lhMockApp \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -946,8 +946,7 @@ testBlockClaimingKeyPackageForLHUsers = do pStatus <- profile %. "legalhold_status" & asString pStatus `shouldMatch` "enabled" - mls <- getMLSState - claimKeyPackages mls.ciphersuite alice1 charlie + claimKeyPackages def alice1 charlie `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" -- | scenario 2.2: @@ -958,8 +957,8 @@ testBlockClaimingKeyPackageForLHUsers = do testBlockCreateMLSConvForLHUsers :: (HasCallStack) => LhApiVersion -> App () testBlockCreateMLSConvForLHUsers v = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 - [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] - _ <- uploadNewKeyPackage alice1 + [alice1, charlie1] <- traverse (createMLSClient def def) [alice, charlie] + _ <- uploadNewKeyPackage def alice1 legalholdWhitelistTeam tid alice >>= assertStatus 200 withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -970,12 +969,12 @@ testBlockCreateMLSConvForLHUsers v = do pStatus `shouldMatch` "enabled" -- charlie tries to create a group and should fail when POSTing the add commit - _ <- createNewGroup charlie1 + convId <- createNewGroup def charlie1 void -- we try to add alice since adding charlie himself would trigger 2.1 -- since he'd try to claim his own keypackages - $ createAddCommit charlie1 [alice] + $ createAddCommit charlie1 convId [alice] >>= \mp -> postMLSCommitBundle mp.sender (mkBundle mp) `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" @@ -983,12 +982,12 @@ testBlockCreateMLSConvForLHUsers v = do -- (unsurprisingly) this same thing should also work in the one2one case respJson <- getMLSOne2OneConversation alice charlie >>= getJSON 200 - resetGroup alice1 (respJson %. "conversation") + createGroup def alice1 =<< objConvId (respJson %. "conversation") void -- we try to add alice since adding charlie himself would trigger 2.1 -- since he'd try to claim his own keypackages - $ createAddCommit charlie1 [alice] + $ createAddCommit charlie1 convId [alice] >>= \mp -> postMLSCommitBundle mp.sender (mkBundle mp) `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index f721f9ad06b..856f480e983 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -6,6 +6,7 @@ import API.Brig (claimKeyPackages, deleteClient) import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -19,15 +20,15 @@ import Testlib.Prelude testSendMessageNoReturnToSender :: (HasCallStack) => App () testSendMessageNoReturnToSender = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle -- alice1 sends a message to the conversation, all clients but alice1 receive -- the message withWebSockets [alice1, alice2, bob1, bob2] $ \(wsSender : wss) -> do - mp <- createApplicationMessage alice1 "hello, bob" + mp <- createApplicationMessage convId alice1 "hello, bob" bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 for_ wss $ \ws -> do @@ -47,25 +48,25 @@ testPastStaleApplicationMessage :: (HasCallStack) => Domain -> App () testPastStaleApplicationMessage otherDomain = do [alice, bob, charlie, dave, eve] <- createAndConnectUsers [OwnDomain, otherDomain, OwnDomain, OwnDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 -- alice adds bob first - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle -- bob prepares some application messages - [msg1, msg2] <- replicateM 2 $ createApplicationMessage bob1 "hi alice" + [msg1, msg2] <- replicateM 2 $ createApplicationMessage convId bob1 "hi alice" -- alice adds charlie and dave with different commits - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle - void $ createAddCommit alice1 [dave] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [dave] >>= sendAndConsumeCommitBundle -- bob's application messages still go through void $ postMLSMessage bob1 msg1.message >>= getJSON 201 -- alice adds eve - void $ createAddCommit alice1 [eve] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [eve] >>= sendAndConsumeCommitBundle -- bob's application messages are now rejected void $ postMLSMessage bob1 msg2.message >>= getJSON 409 @@ -73,20 +74,28 @@ testPastStaleApplicationMessage otherDomain = do testFutureStaleApplicationMessage :: (HasCallStack) => App () testFutureStaleApplicationMessage = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 -- alice adds bob - void . sendAndConsumeCommitBundle =<< createAddCommit alice1 [bob] + void . sendAndConsumeCommitBundle =<< createAddCommit alice1 convId [bob] -- alice adds charlie and consumes the commit without sending it - void $ createAddCommit alice1 [charlie] + void $ createAddCommit alice1 convId [charlie] modifyMLSState $ \mls -> mls - { epoch = epoch mls + 1, - members = members mls <> Set.singleton charlie1, - newMembers = mempty + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = Set.insert charlie1 conv.members, + newMembers = mempty + } + ) + convId + mls.convs } -- alice's application message is rejected @@ -94,7 +103,7 @@ testFutureStaleApplicationMessage = do . getJSON 409 =<< postMLSMessage alice1 . (.message) - =<< createApplicationMessage alice1 "hi bob" + =<< createApplicationMessage convId alice1 "hi bob" testMixedProtocolUpgrade :: (HasCallStack) => Domain -> App () testMixedProtocolUpgrade secondDomain = do @@ -102,7 +111,7 @@ testMixedProtocolUpgrade secondDomain = do [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] - qcnv <- + convId <- postConversation alice defProteus @@ -110,77 +119,79 @@ testMixedProtocolUpgrade secondDomain = do team = Just tid } >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 403 withWebSockets [alice, charlie] $ \websockets -> do - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "conversation" `shouldMatch` (qcnv %. "id") + resp.json %. "qualified_conversation" `shouldMatch` (convIdToQidObject convId) resp.json %. "data.protocol" `shouldMatch` "mixed" - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} for_ websockets $ \ws -> do n <- awaitMatch (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") ws nPayload n %. "data.protocol" `shouldMatch` "mixed" - bindResponse (getConversation alice qcnv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mixed" resp.json %. "epoch" `shouldMatchInt` 0 - bindResponse (putConversationProtocol alice qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol alice convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 204 - bindResponse (putConversationProtocol bob qcnv "proteus") $ \resp -> do + bindResponse (putConversationProtocol bob convId "proteus") $ \resp -> do resp.status `shouldMatchInt` 403 - bindResponse (putConversationProtocol bob qcnv "invalid") $ \resp -> do + bindResponse (putConversationProtocol bob convId "invalid") $ \resp -> do resp.status `shouldMatchInt` 400 testMixedProtocolNonTeam :: (HasCallStack) => Domain -> App () testMixedProtocolNonTeam secondDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, secondDomain] - qcnv <- + convId <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 403 testMixedProtocolAddUsers :: (HasCallStack) => Domain -> Ciphersuite -> App () testMixedProtocolAddUsers secondDomain suite = do - setMLSCiphersuite suite (alice, tid, _) <- createTeam OwnDomain 1 [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "epoch" `shouldMatchInt` 0 + objConvId resp.json - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "epoch" `shouldMatchInt` 0 - createGroup alice1 resp.json + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + createGroup suite alice1 convId - traverse_ uploadNewKeyPackage [bob1] + void $ uploadNewKeyPackage suite bob1 withWebSocket bob $ \ws -> do - mp <- createAddCommit alice1 [bob] + mp <- createAddCommit alice1 convId [bob] welcome <- assertJust "should have welcome" mp.welcome - void $ sendAndConsumeCommitBundle mp + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp n <- awaitMatch (\n -> nPayload n %. "type" `isEqual` "conversation.mls-welcome") ws nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode welcome) - bindResponse (getConversation alice qcnv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "epoch" `shouldMatchInt` 1 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) @@ -192,32 +203,34 @@ testMixedProtocolUserLeaves secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + createGroup def alice1 convId + void $ uploadNewKeyPackage def bob1 - mp <- createAddCommit alice1 [bob] - void $ sendAndConsumeCommitBundle mp + mp <- createAddCommit alice1 convId [bob] + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp withWebSocket alice $ \ws -> do - bindResponse (removeConversationMember bob qcnv) $ \resp -> + bindResponse (removeConversationMember bob (convIdToQidObject convId)) $ \resp -> resp.status `shouldMatchInt` 200 n <- awaitMatch (\n -> nPayload n %. "type" `isEqual` "conversation.mls-message-add") ws - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + conv <- getMLSConv convId + msg <- asByteString (nPayload n %. "data") >>= showMessage conv.ciphersuite alice1 let leafIndexBob = 1 msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob msg %. "message.content.sender.External" `shouldMatchInt` 0 @@ -228,29 +241,31 @@ testMixedProtocolAddPartialClients secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + createGroup def alice1 convId - traverse_ uploadNewKeyPackage [bob1, bob1, bob2, bob2] + traverse_ (uploadNewKeyPackage def) [bob1, bob1, bob2, bob2] -- create add commit for only one of bob's two clients do bundle <- claimKeyPackages def alice1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp1 <- assertOne (filter ((== bob1) . fst) kps) - mp <- createAddCommitWithKeyPackages alice1 [kp1] - void $ sendAndConsumeCommitBundle mp + mp <- createAddCommitWithKeyPackages alice1 convId [kp1] + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp -- this tests that bob's backend has a mapping of group id to the remote conv -- this test is only interesting when bob is on OtherDomain @@ -258,7 +273,7 @@ testMixedProtocolAddPartialClients secondDomain = do bundle <- claimKeyPackages def bob1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp2 <- assertOne (filter ((== bob2) . fst) kps) - mp <- createAddCommitWithKeyPackages bob1 [kp2] + mp <- createAddCommitWithKeyPackages bob1 convId [kp2] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 testMixedProtocolRemovePartialClients :: (HasCallStack) => Domain -> App () @@ -267,23 +282,24 @@ testMixedProtocolRemovePartialClients secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - mp <- createRemoveCommit alice1 [bob1] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + createGroup def alice1 convId + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + mp <- createRemoveCommit alice1 convId [bob1] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 @@ -293,83 +309,83 @@ testMixedProtocolAppMessagesAreDenied secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + createGroup def alice1 convId + void $ uploadNewKeyPackage def bob1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - mp <- createApplicationMessage bob1 "hello, world" + mp <- createApplicationMessage convId bob1 "hello, world" bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 422 resp.json %. "label" `shouldMatch` "mls-unsupported-message" testMLSProtocolUpgrade :: (HasCallStack) => Domain -> App () testMLSProtocolUpgrade secondDomain = do - (alice, bob, conv) <- simpleMixedConversationSetup secondDomain + (alice, bob, convId) <- simpleMixedConversationSetup secondDomain charlie <- randomUser OwnDomain def -- alice creates MLS group and bob joins - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - createGroup alice1 conv - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed void $ withWebSocket bob $ \ws -> do -- charlie is added to the group - void $ uploadNewKeyPackage charlie1 - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed awaitMatch isNewMLSMessageNotif ws supportMLS alice - bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" - bindResponse (getConversation alice conv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mixed" supportMLS bob withWebSockets [alice1, bob1] $ \wss -> do - bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMLS} for_ wss $ \ws -> do n <- awaitMatch isNewMLSMessageNotif ws - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 let leafIndexCharlie = 2 msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie msg %. "message.content.sender.External" `shouldMatchInt` 0 - bindResponse (getConversation alice conv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mls" testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - bob1 <- createMLSClient def {credType = ctype} bob - void $ uploadNewKeyPackage bob1 - [alice1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob] + bob1 <- createMLSClient suite def {credType = ctype} bob + void $ uploadNewKeyPackage suite bob1 + [alice1, bob2] <- traverse (createMLSClient suite def {credType = ctype}) [alice, bob] - traverse_ uploadNewKeyPackage [bob2] + void $ uploadNewKeyPackage suite bob2 qcnv <- withWebSocket alice $ \ws -> do - (_, qcnv) <- createNewGroup alice1 + qcnv <- createNewGroup suite alice1 -- check that the conversation inside the ConvCreated event contains -- epoch and ciphersuite, regardless of the API version n <- awaitMatch isConvCreateNotif ws @@ -377,11 +393,12 @@ testAddUserSimple suite ctype = do n %. "payload.0.data.cipher_suite" `shouldMatchInt` 1 pure qcnv - resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + resp <- createAddCommit alice1 qcnv [bob] >>= sendAndConsumeCommitBundle events <- resp %. "events" & asList do event <- assertOne events - shouldMatch (event %. "qualified_conversation") qcnv + shouldMatch (event %. "qualified_conversation.id") qcnv.id_ + shouldMatch (event %. "qualified_conversation.domain") qcnv.domain shouldMatch (event %. "type") "conversation.member-join" shouldMatch (event %. "from") (objId alice) members <- event %. "data" %. "users" & asList @@ -391,7 +408,7 @@ testAddUserSimple suite ctype = do -- check that bob can now see the conversation convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs void $ assertBool "Users added to an MLS group should find it when listing conversations" @@ -400,14 +417,14 @@ testAddUserSimple suite ctype = do testRemoteAddUser :: (HasCallStack) => App () testRemoteAddUser = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - bindResponse (updateConversationMember alice1 conv bob "wire_admin") $ \resp -> + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle + bindResponse (updateConversationMember alice1 (convIdToQidObject conv) bob "wire_admin") $ \resp -> resp.status `shouldMatchInt` 200 - mp <- createAddCommit bob1 [charlie] + mp <- createAddCommit bob1 conv [charlie] -- Support for remote admins is not implemeted yet, but this shows that add -- proposal is being applied action bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do @@ -416,115 +433,113 @@ testRemoteAddUser = do testRemoteRemoveClient :: (HasCallStack) => Ciphersuite -> App () testRemoteRemoveClient suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 + conv <- createNewGroup suite alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle withWebSocket alice $ \wsAlice -> do void $ deleteClient bob bob1.client >>= getBody 200 let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch predicate wsAlice - shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject conv) shouldMatch (nPayload n %. "from") (objId bob) mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume conv suite alice1 mlsMsg -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like -- remove proposals from the backend. We should fix that in future. -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage suite alice1 mlsMsg let leafIndexBob = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testRemoteRemoveCreatorClient :: (HasCallStack) => Ciphersuite -> App () testRemoteRemoveCreatorClient suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 + conv <- createNewGroup suite alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle withWebSocket bob $ \wsBob -> do void $ deleteClient alice alice1.client >>= getBody 200 let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch predicate wsBob - shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject conv) shouldMatch (nPayload n %. "from") (objId alice) mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume conv suite alice1 mlsMsg -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like -- remove proposals from the backend. We should fix that in future. -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage suite alice1 mlsMsg let leafIndexAlice = 0 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testCreateSubConv :: (HasCallStack) => Ciphersuite -> App () testCreateSubConv suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - aliceClients@(alice1 : _) <- replicateM 5 $ createMLSClient def alice - replicateM_ 3 $ traverse_ uploadNewKeyPackage aliceClients - [bob1, bob2] <- replicateM 2 $ createMLSClient def bob - replicateM_ 3 $ traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference" + aliceClients@(alice1 : _) <- replicateM 5 $ createMLSClient suite def alice + replicateM_ 3 $ traverse_ (uploadNewKeyPackage suite) aliceClients + [bob1, bob2] <- replicateM 2 $ createMLSClient suite def bob + replicateM_ 3 $ traverse_ (uploadNewKeyPackage suite) [bob1, bob2] + convId <- createNewGroup suite alice1 + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle + createSubConv suite convId alice1 "conference" testCreateSubConvProteus :: App () testCreateSubConvProteus = do alice <- randomUser OwnDomain def conv <- bindResponse (postConversation alice defProteus) $ \resp -> do resp.status `shouldMatchInt` 201 - resp.json + objConvId resp.json bindResponse (getSubConversation alice conv "conference") $ \resp -> resp.status `shouldMatchInt` 404 testSelfConversation :: Version5 -> App () testSelfConversation v = withVersion5 v $ do alice <- randomUser OwnDomain def - creator : others <- traverse (createMLSClient def) (replicate 3 alice) - traverse_ uploadNewKeyPackage others - (_, conv) <- createSelfGroup creator + creator : others <- traverse (createMLSClient def def) (replicate 3 alice) + traverse_ (uploadNewKeyPackage def) others + (_, conv) <- createSelfGroup def creator + convId <- objConvId conv conv %. "epoch" `shouldMatchInt` 0 case v of Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 NoVersion5 -> assertFieldMissing conv "cipher_suite" - void $ createAddCommit creator [alice] >>= sendAndConsumeCommitBundle + void $ createAddCommit creator convId [alice] >>= sendAndConsumeCommitBundle - newClient <- createMLSClient def alice - void $ uploadNewKeyPackage newClient - void $ createExternalCommit newClient Nothing >>= sendAndConsumeCommitBundle + newClient <- createMLSClient def def alice + void $ uploadNewKeyPackage def newClient + void $ createExternalCommit convId newClient Nothing >>= sendAndConsumeCommitBundle -- | FUTUREWORK: Don't allow partial adds, not even in the first commit testFirstCommitAllowsPartialAdds :: (HasCallStack) => App () testFirstCommitAllowsPartialAdds = do alice <- randomUser OwnDomain def - [alice1, alice2, alice3] <- traverse (createMLSClient def) [alice, alice, alice] - traverse_ uploadNewKeyPackage [alice1, alice2, alice2, alice3, alice3] + [alice1, alice2, alice3] <- traverse (createMLSClient def def) [alice, alice, alice] + traverse_ (uploadNewKeyPackage def) [alice1, alice2, alice2, alice3, alice3] - (_, _qcnv) <- createNewGroup alice1 + convId <- createNewGroup def alice1 bundle <- claimKeyPackages def alice1 alice >>= getJSON 200 kps <- unbundleKeyPackages bundle -- first commit only adds kp for alice2 (not alice2 and alice3) - mp <- createAddCommitWithKeyPackages alice1 (filter ((== alice2) . fst) kps) + mp <- createAddCommitWithKeyPackages alice1 convId (filter ((== alice2) . fst) kps) bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-client-mismatch" @@ -538,24 +553,24 @@ testAddUserPartial = do [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) -- Bob has 3 clients, Charlie has 2 - alice1 <- createMLSClient def alice - bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient def bob) - charlieClients <- replicateM 2 (createMLSClient def charlie) + alice1 <- createMLSClient def def alice + bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient def def bob) + charlieClients <- replicateM 2 (createMLSClient def def charlie) -- Only the first 2 clients of Bob's have uploaded key packages - traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) + traverse_ (uploadNewKeyPackage def) (take 2 bobClients <> charlieClients) -- alice adds bob's first 2 clients - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 -- alice sends a commit now, and should get a conflict error kps <- fmap concat . for [bob, charlie] $ \user -> do bundle <- claimKeyPackages def alice1 user >>= getJSON 200 unbundleKeyPackages bundle - mp <- createAddCommitWithKeyPackages alice1 kps + mp <- createAddCommitWithKeyPackages alice1 convId kps -- before alice can commit, bob3 uploads a key package - void $ uploadNewKeyPackage bob3 + void $ uploadNewKeyPackage def bob3 err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" @@ -567,30 +582,30 @@ testRemoveClientsIncomplete :: (HasCallStack) => App () testRemoveClientsIncomplete = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - mp <- createRemoveCommit alice1 [bob1] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + mp <- createRemoveCommit alice1 convId [bob1] err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" testAdminRemovesUserFromConv :: (HasCallStack) => Ciphersuite -> App () testAdminRemovesUserFromConv suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient suite def) [alice, bob, bob] - void $ createWireClient bob - traverse_ uploadNewKeyPackage [bob1, bob2] - (gid, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle + void $ createWireClient bob def + traverse_ (uploadNewKeyPackage suite) [bob1, bob2] + convId <- createNewGroup suite alice1 + let Just gid = convId.groupId + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + events <- createRemoveCommit alice1 convId [bob1, bob2] >>= sendAndConsumeCommitBundle do event <- assertOne =<< asList (events %. "events") - event %. "qualified_conversation" `shouldMatch` qcnv + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId event %. "type" `shouldMatch` "conversation.member-leave" event %. "from" `shouldMatch` objId alice members <- event %. "data" %. "qualified_user_ids" & asList @@ -599,26 +614,26 @@ testAdminRemovesUserFromConv suite = do do convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs clients <- bindResponse (getGroupClients alice gid) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "client_ids" & asList void $ assertOne clients assertBool "bob is not longer part of conversation after the commit" - (qcnv `notElem` convIds) + (convId `notElem` convIds) testLocalWelcome :: (HasCallStack) => App () testLocalWelcome = do users@[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1] <- traverse (createMLSClient def) users + [alice1, bob1] <- traverse (createMLSClient def def) users - void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage def bob1 - (_, qcnv) <- createNewGroup alice1 + convId <- createNewGroup def alice1 - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 convId [bob] Just welcome <- pure commit.welcome es <- withWebSocket bob1 $ \wsBob -> do @@ -627,14 +642,14 @@ testLocalWelcome = do n <- awaitMatch isWelcome wsBob - shouldMatch (nPayload n %. "conversation") (objId qcnv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject convId) shouldMatch (nPayload n %. "from") (objId alice) shouldMatch (nPayload n %. "data") (B8.unpack (Base64.encode welcome)) pure es event <- assertOne =<< asList (es %. "events") event %. "type" `shouldMatch` "conversation.member-join" - event %. "conversation" `shouldMatch` objId qcnv + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId addedUser <- (event %. "data.users") >>= asList >>= assertOne objQid addedUser `shouldMatch` objQid bob @@ -643,19 +658,19 @@ testStaleCommit = do (alice : users) <- createAndConnectUsers (replicate 5 OwnDomain) let (users1, users2) = splitAt 2 users - (alice1 : clients) <- traverse (createMLSClient def) (alice : users) - traverse_ uploadNewKeyPackage clients - void $ createNewGroup alice1 + (alice1 : clients) <- traverse (createMLSClient def def) (alice : users) + traverse_ (uploadNewKeyPackage def) clients + convId <- createNewGroup def alice1 gsBackup <- getClientGroupState alice1 -- add the first batch of users to the conversation - void $ createAddCommit alice1 users1 >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId users1 >>= sendAndConsumeCommitBundle -- now roll back alice1 and try to add the second batch of users setClientGroupState alice1 gsBackup - mp <- createAddCommit alice1 users2 + mp <- createAddCommit alice1 convId users2 bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" @@ -663,54 +678,54 @@ testStaleCommit = do testPropInvalidEpoch :: (HasCallStack) => App () testPropInvalidEpoch = do users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 OwnDomain) - [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) users - void $ createNewGroup alice1 + [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def def) users + convId <- createNewGroup def alice1 -- Add bob -> epoch 1 - void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage def bob1 gsBackup <- getClientGroupState alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle gsBackup2 <- getClientGroupState alice1 -- try to send a proposal from an old epoch (0) do setClientGroupState alice1 gsBackup - void $ uploadNewKeyPackage dee1 - [prop] <- createAddProposals alice1 [dee] + void $ uploadNewKeyPackage def dee1 + [prop] <- createAddProposals convId alice1 [dee] bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" -- try to send a proposal from a newer epoch (2) do - void $ uploadNewKeyPackage dee1 - void $ uploadNewKeyPackage charlie1 + void $ uploadNewKeyPackage def dee1 + void $ uploadNewKeyPackage def charlie1 setClientGroupState alice1 gsBackup2 - void $ createAddCommit alice1 [charlie] -- --> epoch 2 - [prop] <- createAddProposals alice1 [dee] + void $ createAddCommit alice1 convId [charlie] -- --> epoch 2 + [prop] <- createAddProposals convId alice1 [dee] bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" -- remove charlie from users expected to get a welcome message - modifyMLSState $ \mls -> mls {newMembers = mempty} + modifyMLSState $ \mls -> mls {convs = Map.adjust (\conv -> conv {newMembers = mempty}) convId mls.convs} -- alice send a well-formed proposal and commits it - void $ uploadNewKeyPackage dee1 + void $ uploadNewKeyPackage def dee1 setClientGroupState alice1 gsBackup2 - createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + createAddProposals convId alice1 [dee] >>= traverse_ sendAndConsumeMessage + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle --- | This test submits a ReInit proposal, which is currently ignored by the -- backend, in order to check that unsupported proposal types are accepted. testPropUnsupported :: (HasCallStack) => App () testPropUnsupported = do users@[_alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) users - void $ uploadNewKeyPackage bob1 - void $ createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) users + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - mp <- createReInitProposal alice1 + mp <- createReInitProposal convId alice1 -- we cannot consume this message, because the membership tag is fake void $ postMLSMessage mp.sender mp.message >>= getJSON 201 @@ -718,33 +733,33 @@ testPropUnsupported = do testAddUserBareProposalCommit :: (HasCallStack) => App () testAddUserBareProposalCommit = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - (_, qcnv) <- createNewGroup alice1 - void $ uploadNewKeyPackage bob1 - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + convId <- createNewGroup def alice1 + void $ uploadNewKeyPackage def bob1 + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle - createAddProposals alice1 [bob] + createAddProposals convId alice1 [bob] >>= traverse_ sendAndConsumeMessage - commit <- createPendingProposalCommit alice1 + commit <- createPendingProposalCommit convId alice1 void $ assertJust "Expected welcome" commit.welcome void $ sendAndConsumeCommitBundle commit -- check that bob can now see the conversation convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs void $ assertBool "Users added to an MLS group should find it when listing conversations" - (qcnv `elem` convIds) + (convId `elem` convIds) testPropExistingConv :: (HasCallStack) => App () testPropExistingConv = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - void $ createNewGroup alice1 - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle - res <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle + res <- createAddProposals convId alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne shouldBeEmpty (res %. "events") -- @SF.Separation @TSFI.RESTfulAPI @S2 @@ -755,20 +770,20 @@ testCommitNotReferencingAllProposals :: (HasCallStack) => App () testCommitNotReferencingAllProposals = do users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) - [alice1, bob1, charlie1] <- traverse (createMLSClient def) users - void $ createNewGroup alice1 - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) users + convId <- createNewGroup def alice1 + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle gsBackup <- getClientGroupState alice1 -- create proposals for bob and charlie - createAddProposals alice1 [bob, charlie] + createAddProposals convId alice1 [bob, charlie] >>= traverse_ sendAndConsumeMessage -- now create a commit referencing only the first proposal setClientGroupState alice1 gsBackup - commit <- createPendingProposalCommit alice1 + commit <- createPendingProposalCommit convId alice1 -- send commit and expect and error bindResponse (postMLSCommitBundle alice1 (mkBundle commit)) $ \resp -> do @@ -779,12 +794,12 @@ testCommitNotReferencingAllProposals = do testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do - setMLSCiphersuite (Ciphersuite "0x0003") + let suite = (Ciphersuite "0x0003") alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice - void $ createNewGroup alice1 + alice1 <- createMLSClient suite def alice + convId <- createNewGroup suite alice1 - mp <- createPendingProposalCommit alice1 + mp <- createPendingProposalCommit convId alice1 bindResponse (postMLSCommitBundle alice1 (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 400 @@ -792,32 +807,35 @@ testUnsupportedCiphersuite = do testBackendRemoveProposal :: (HasCallStack) => Ciphersuite -> Domain -> App () testBackendRemoveProposal suite domain = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, domain] - (alice1 : bobClients) <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage bobClients - void $ createNewGroup alice1 + (alice1 : bobClients) <- traverse (createMLSClient suite def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage suite) bobClients + convId <- createNewGroup suite alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle let isRemoveProposalFor :: Int -> Value -> App Bool isRemoveProposalFor index e = isNewMLSMessageNotif e &&~ do msgData <- e %. "payload.0.data" & asByteString - msg <- showMessage alice1 msgData + msg <- showMessage suite alice1 msgData fieldEquals msg "message.content.body.Proposal.Remove.removed" index withWebSocket alice1 \ws -> do deleteUser bob for_ (zip [1 ..] bobClients) \(index, _) -> do - void $ consumeMessageWithPredicate (isRemoveProposalFor index) alice1 Nothing ws + void $ consumeMessageWithPredicate (isRemoveProposalFor index) convId suite alice1 Nothing ws bobUser <- asString $ bob %. "id" modifyMLSState $ \mls -> mls - { members = Set.filter (\m -> m.user /= bobUser) mls.members + { convs = + Map.adjust + (\conv -> conv {members = Set.filter (\m -> m.user /= bobUser) conv.members}) + convId + mls.convs } -- alice commits the external proposals - r <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + r <- createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle shouldBeEmpty $ r %. "events" diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 5f95025a1da..2c49f0206d7 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -12,8 +12,8 @@ import Testlib.Prelude testDeleteKeyPackages :: App () testDeleteKeyPackages = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice - kps <- replicateM 3 (uploadNewKeyPackage alice1) + alice1 <- createMLSClient def def alice + kps <- replicateM 3 (uploadNewKeyPackage def alice1) -- add an extra non-existing key package to the delete request let kps' = "4B701F521EBE82CEC4AD5CB67FDD8E1C43FC4868DE32D03933CE4993160B75E8" : kps @@ -28,13 +28,12 @@ testDeleteKeyPackages = do testKeyPackageMultipleCiphersuites :: App () testKeyPackageMultipleCiphersuites = do alice <- randomUser OwnDomain def - [alice1, alice2] <- replicateM 2 (createMLSClient def alice) + [alice1, alice2] <- replicateM 2 (createMLSClient def def alice) - kp <- uploadNewKeyPackage alice2 + kp <- uploadNewKeyPackage def alice2 let suite = Ciphersuite "0xf031" - setMLSCiphersuite suite - void $ uploadNewKeyPackage alice2 + void $ uploadNewKeyPackage suite alice2 -- count key packages with default ciphersuite bindResponse (countKeyPackages def alice2) $ \resp -> do @@ -54,9 +53,9 @@ testKeyPackageMultipleCiphersuites = do testKeyPackageUploadNoKey :: App () testKeyPackageUploadNoKey = do alice <- randomUser OwnDomain def - alice1 <- createWireClient alice + alice1 <- createWireClient alice def - (kp, _) <- generateKeyPackage alice1 + (kp, _) <- generateKeyPackage alice1 def -- if we upload a keypackage without a key, -- we get a bad request @@ -73,14 +72,14 @@ testKeyPackageClaim :: App () testKeyPackageClaim = do alice <- randomUser OwnDomain def alices@[alice1, _alice2] <- replicateM 2 do - createMLSClient def alice + createMLSClient def def alice for_ alices \alicei -> replicateM 3 do - uploadNewKeyPackage alicei + uploadNewKeyPackage def alicei bob <- randomUser OwnDomain def bobs <- replicateM 3 do - createMLSClient def bob + createMLSClient def def bob for_ bobs \bobi -> claimKeyPackages def bobi alice `bindResponse` \resp -> do @@ -109,9 +108,9 @@ testKeyPackageSelfClaim :: App () testKeyPackageSelfClaim = do alice <- randomUser OwnDomain def alices@[alice1, alice2] <- replicateM 2 do - createMLSClient def alice + createMLSClient def def alice for_ alices \alicei -> replicateM 3 do - uploadNewKeyPackage alicei + uploadNewKeyPackage def alicei -- claim own keypackages claimKeyPackages def alice1 alice `bindResponse` \resp -> do @@ -133,7 +132,7 @@ testKeyPackageSelfClaim = do bob <- randomUser OwnDomain def bobs <- replicateM 2 do - createMLSClient def bob + createMLSClient def def bob -- skip own should only apply to own keypackages, hence -- bob claiming alices keypackages should work as normal @@ -152,13 +151,13 @@ testKeyPackageSelfClaim = do testKeyPackageRemoteClaim :: App () testKeyPackageRemoteClaim = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice + alice1 <- createMLSClient def def alice charlie <- randomUser OtherDomain def - charlie1 <- createMLSClient def charlie + charlie1 <- createMLSClient def def charlie - refCharlie <- uploadNewKeyPackage charlie1 - refAlice <- uploadNewKeyPackage alice1 + refCharlie <- uploadNewKeyPackage def charlie1 + refAlice <- uploadNewKeyPackage def alice1 -- the user should be able to claim the keypackage of -- a remote user and vice versa @@ -180,30 +179,28 @@ testKeyPackageRemoteClaim = do resp.status `shouldMatchInt` 200 testKeyPackageCount :: (HasCallStack) => Ciphersuite -> App () -testKeyPackageCount cs = do - setMLSCiphersuite cs +testKeyPackageCount suite = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice + alice1 <- createMLSClient suite def alice - bindResponse (countKeyPackages cs alice1) $ \resp -> do + bindResponse (countKeyPackages suite alice1) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` 0 let count = 10 - kps <- map fst <$> replicateM count (generateKeyPackage alice1) + kps <- map fst <$> replicateM count (generateKeyPackage alice1 suite) void $ uploadKeyPackages alice1 kps >>= getBody 201 - bindResponse (countKeyPackages cs alice1) $ \resp -> do + bindResponse (countKeyPackages suite alice1) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` count testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do let suite = Ciphersuite "0x0003" - setMLSCiphersuite suite bob <- randomUser OwnDomain def - bob1 <- createMLSClient def bob - (kp, _) <- generateKeyPackage bob1 + bob1 <- createMLSClient suite def bob + (kp, _) <- generateKeyPackage bob1 suite bindResponse (uploadKeyPackages bob1 [kp]) $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-protocol-error" @@ -211,7 +208,7 @@ testUnsupportedCiphersuite = do testReplaceKeyPackages :: (HasCallStack) => App () testReplaceKeyPackages = do alice <- randomUser OwnDomain def - [alice1, alice2] <- replicateM 2 $ createMLSClient def alice + [alice1, alice2] <- replicateM 2 $ createMLSClient def def alice let suite = Ciphersuite "0xf031" let checkCount cs n = @@ -221,12 +218,11 @@ testReplaceKeyPackages = do -- setup: upload a batch of key packages for each ciphersuite void - $ replicateM 4 (fmap fst (generateKeyPackage alice1)) + $ replicateM 4 (fmap fst (generateKeyPackage alice1 def)) >>= uploadKeyPackages alice1 >>= getBody 201 - setMLSCiphersuite suite void - $ replicateM 5 (fmap fst (generateKeyPackage alice1)) + $ replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -235,7 +231,7 @@ testReplaceKeyPackages = do do -- generate a new batch of key packages - (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1) + (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1 suite) -- replace old key packages with new void $ replaceKeyPackages alice1 (Just [suite]) kps >>= getBody 201 @@ -261,7 +257,7 @@ testReplaceKeyPackages = do do -- replenish key packages for the second ciphersuite void - $ replicateM 5 (fmap fst (generateKeyPackage alice1)) + $ replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -269,10 +265,8 @@ testReplaceKeyPackages = do checkCount suite 5 -- replace all key packages with fresh ones - setMLSCiphersuite def - kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) - setMLSCiphersuite suite - kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) + kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1 def)) + kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1 suite)) void $ replaceKeyPackages alice1 (Just [def, suite]) (kps1 <> kps2) >>= getBody 201 @@ -280,10 +274,8 @@ testReplaceKeyPackages = do checkCount suite 2 do - setMLSCiphersuite def - defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) - setMLSCiphersuite suite - suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1 def)) + suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1 suite)) void $ replaceKeyPackages alice1 (Just []) [] diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index 81a194d3674..47708a53984 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -42,59 +42,59 @@ testApplicationMessage = do clients@[alice1, _alice2, alex1, _alex2, bob1, _bob2, _, _] <- traverse - (createMLSClient def) + (createMLSClient def def) [alice, alice, alex, alex, bob, bob, betty, betty] - traverse_ uploadNewKeyPackage clients - void $ createNewGroup alice1 + traverse_ (uploadNewKeyPackage def) clients + convId <- createNewGroup def alice1 withWebSockets [alice, alex, bob, betty] $ \wss -> do -- alice adds all other users (including her own client) - void $ createAddCommit alice1 [alice, alex, bob, betty] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, alex, bob, betty] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss -- alex sends a message - void $ createApplicationMessage alex1 "hello" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alex1 "hello" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- bob sends a message - void $ createApplicationMessage bob1 "hey" >>= sendAndConsumeMessage + void $ createApplicationMessage convId bob1 "hey" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- @END testAppMessageSomeReachable :: (HasCallStack) => App () testAppMessageSomeReachable = do - alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do + (alice1, convId) <- startDynamicBackends [mempty] $ \[thirdDomain] -> do ownDomain <- make OwnDomain & asString otherDomain <- make OtherDomain & asString [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 void $ withWebSocket charlie $ \ws -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - pure alice1 + pure (alice1, convId) -- charlie isn't able to receive this message, so we make sure we can post it -- successfully, but not attempt to consume it - mp <- createApplicationMessage alice1 "hi, bob!" + mp <- createApplicationMessage convId alice1 "hi, bob!" void $ postMLSMessage mp.sender mp.message >>= getJSON 201 testMessageNotifications :: (HasCallStack) => Domain -> App () testMessageNotifications bobDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, bobDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] bobClient <- bob1 %. "client_id" & asString - traverse_ uploadNewKeyPackage [alice1, alice2, bob1, bob2] + traverse_ (uploadNewKeyPackage def) [alice1, alice2, bob1, bob2] - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 void $ withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws let get (opts :: GetNotifications) = do @@ -106,7 +106,7 @@ testMessageNotifications bobDomain = do numNotifsClient <- get def {client = Just bobClient} void $ withWebSocket bob $ \ws -> do - void $ createApplicationMessage alice1 "hi bob" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "hi bob" >>= sendAndConsumeMessage awaitMatch isNewMLSMessageNotif ws get def `shouldMatchInt` (numNotifs + 1) @@ -115,16 +115,16 @@ testMessageNotifications bobDomain = do testMultipleMessages :: (HasCallStack) => App () testMultipleMessages = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1, bob1] - void $ createNewGroup alice1 + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + traverse_ (uploadNewKeyPackage def) [alice1, bob1] + convId <- createNewGroup def alice1 withWebSockets [bob] $ \wss -> do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss - void $ createApplicationMessage alice1 "hello" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "hello" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss - void $ createApplicationMessage alice1 "world" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "world" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss diff --git a/integration/test/Test/MLS/Notifications.hs b/integration/test/Test/MLS/Notifications.hs index 61a0b60d53f..a75d72276d2 100644 --- a/integration/test/Test/MLS/Notifications.hs +++ b/integration/test/Test/MLS/Notifications.hs @@ -9,12 +9,12 @@ import Testlib.Prelude testWelcomeNotification :: (HasCallStack) => App () testWelcomeNotification = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2] - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 notif <- withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle awaitMatch isWelcomeNotif ws notifId <- notif %. "id" & asString diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index d93e5f582c2..660368b7660 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -23,6 +23,7 @@ import Control.Concurrent.Async import Control.Concurrent.MVar import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Read as T @@ -116,16 +117,17 @@ testMLSOne2OneOtherMember scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" do convId <- one2OneConv %. "conversation.qualified_id" bobOne2OneConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobOne2OneConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \ws -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" n <- awaitMatch isMessage ws @@ -151,11 +153,12 @@ testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] conv <- getMLSOne2OneConversationLegacy alice bob >>= getJSON 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- objConvId conv + createGroup def alice1 convId - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle withWebSocket alice $ \wsAlice -> do _ <- deleteClient bob bob1.client >>= getBody 200 @@ -167,9 +170,9 @@ testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume convId def alice1 mlsMsg - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage def alice1 mlsMsg let leafIndexBob = 1 -- msg `shouldMatch` "foo" parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob @@ -198,15 +201,16 @@ testMLSOne2OneBlockedAfterConnected scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" convId <- one2OneConv %. "conversation.qualified_id" do bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv - commit <- createAddCommit alice1 [bob] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv + commit <- createAddCommit alice1 one2OneConvId [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -223,7 +227,7 @@ testMLSOne2OneBlockedAfterConnected scenario = do -- Bob. void $ getMLSOne2OneConversation alice bob >>= getJSON 403 - mp <- createApplicationMessage bob1 "hello, world, again" + mp <- createApplicationMessage one2OneConvId bob1 "hello, world, again" withWebSocket alice1 $ \ws -> do void $ postMLSMessage mp.sender mp.message >>= getJSON 201 awaitAnyEvent 2 ws `shouldMatch` (Nothing :: Maybe Value) @@ -237,16 +241,17 @@ testMLSOne2OneUnblocked scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" do convId <- one2OneConv %. "conversation.qualified_id" bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \ws -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" n <- awaitMatch isMessage ws @@ -259,24 +264,24 @@ testMLSOne2OneUnblocked scenario = do -- Reset the group membership in the test setup as only 'bob1' is left in -- reality, even though the test state believes 'alice1' is still part of the -- conversation. - modifyMLSState $ \s -> s {members = Set.singleton bob1} + modifyMLSState $ \s -> s {convs = Map.adjust (\conv -> conv {members = Set.singleton bob1}) one2OneConvId s.convs} -- Bob creates a new client and adds it to the one-to-one conversation just so -- that the epoch advances. - bob2 <- createMLSClient def bob - traverse_ uploadNewKeyPackage [bob2] - void $ createAddCommit bob1 [bob] >>= sendAndConsumeCommitBundle + bob2 <- createMLSClient def def bob + void $ uploadNewKeyPackage def bob2 + void $ createAddCommit bob1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle -- Alice finally unblocks Bob void $ putConnection alice bob "accepted" >>= getBody 200 void $ getMLSOne2OneConversation alice bob >>= getJSON 200 -- Alice rejoins via an external commit - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit one2OneConvId alice1 Nothing >>= sendAndConsumeCommitBundle -- Check that an application message can get to Bob withWebSockets [bob1, bob2] $ \wss -> do - mp <- createApplicationMessage alice1 "hello, I've always been here" + mp <- createApplicationMessage one2OneConvId alice1 "hello, I've always been here" void $ sendAndConsumeMessage mp let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" forM_ wss $ \ws -> do @@ -316,18 +321,18 @@ one2OneScenarioConvDomain One2OneScenarioRemoteConv = OtherDomain testMLSOne2One :: (HasCallStack) => Ciphersuite -> One2OneScenario -> App () testMLSOne2One suite scenario = do - setMLSCiphersuite suite alice <- randomUser OwnDomain def let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup suite alice1 one2OneConv - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit @@ -338,7 +343,7 @@ testMLSOne2One suite scenario = do void $ awaitMatch isMemberJoinNotif ws withWebSocket bob1 $ \ws -> do - mp <- createApplicationMessage alice1 "hello, world" + mp <- createApplicationMessage one2OneConvId alice1 "hello, world" void $ sendAndConsumeMessage mp let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch isMessage ws @@ -346,7 +351,7 @@ testMLSOne2One suite scenario = do -- Send another commit. This verifies that the backend has correctly updated -- the cipersuite of this conversation. - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit one2OneConvId alice1 >>= sendAndConsumeCommitBundle one2OneConv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) @@ -360,10 +365,11 @@ testMLSOne2One suite scenario = do testMLSGhostOne2OneConv :: App () testMLSGhostOne2OneConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup def alice1 one2OneConv doneVar <- liftIO $ newEmptyMVar let checkConversation = @@ -379,7 +385,7 @@ testMLSGhostOne2OneConv = do createCommit <- appToIO $ void - $ createAddCommit alice1 [bob] + $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle liftIO $ withAsync checkConversationIO $ \a -> do @@ -409,8 +415,8 @@ testMLSFederationV1ConvOnOldBackend = do else createBob bob <- createBob - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def alice1 -- Alice cannot start this conversation because it would exist on Bob's -- backend and Alice cannot get the MLS public keys of that backend. @@ -419,11 +425,12 @@ testMLSFederationV1ConvOnOldBackend = do fedError %. "label" `shouldMatch` "federation-version-error" conv <- getMLSOne2OneConversationLegacy bob alice >>= getJSON 200 + convId <- objConvId conv keys <- getMLSPublicKeys bob >>= getJSON 200 - resetOne2OneGroupGeneric bob1 conv keys + resetOne2OneGroupGeneric def bob1 conv keys withWebSocket alice1 $ \wsAlice -> do - commit <- createAddCommit bob1 [alice] + commit <- createAddCommit bob1 convId [alice] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -441,9 +448,9 @@ testMLSFederationV1ConvOnOldBackend = do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by bob - void $ mlsCliConsume bob1 mlsMsg + void $ mlsCliConsume convId def bob1 mlsMsg - parsedMsg <- showMessage bob1 mlsMsg + parsedMsg <- showMessage def bob1 mlsMsg let leafIndexAlice = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 @@ -463,8 +470,8 @@ testMLSFederationV1ConvOnNewBackend = do else createBob bob <- createBob - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 -- Bob cannot start this conversation because it would exist on Alice's -- backend and Bob cannot get the MLS public keys of that backend. @@ -473,11 +480,12 @@ testMLSFederationV1ConvOnNewBackend = do fedError %. "label" `shouldMatch` "federation-remote-error" one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" conv <- one2OneConv %. "conversation" - resetOne2OneGroup alice1 one2OneConv + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \wsBob -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -495,9 +503,9 @@ testMLSFederationV1ConvOnNewBackend = do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by bob - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume one2OneConvId def alice1 mlsMsg - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage def alice1 mlsMsg let leafIndexBob = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 83c5376edf3..40cf1e66bf0 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -3,6 +3,7 @@ module Test.MLS.SubConversation where import API.Galley import Control.Monad.Trans (lift) import Control.Monad.Trans.Maybe (MaybeT (runMaybeT)) +import qualified Data.Map as Map import qualified Data.Set as Set import MLS.Util import Notifications @@ -13,44 +14,47 @@ import Testlib.Prelude testJoinSubConv :: App () testJoinSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- createNewGroup alice1 + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + convId <- createNewGroup def alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createSubConv bob1 "conference" + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + void $ createSubConv def convId bob1 "conference" -- bob adds his first client to the subconversation - sub' <- getSubConversation bob qcnv "conference" >>= getJSON 200 + sub' <- getSubConversation bob convId "conference" >>= getJSON 200 + subConvId <- objConvId sub' do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) -- now alice joins with her own client void - $ createExternalCommit alice1 Nothing + $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle testJoinOne2OneSubConv :: App () testJoinOne2OneSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId (one2OneConv %. "conversation") + resetOne2OneGroup def alice1 one2OneConv - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createOne2OneSubConv bob1 "conference" (one2OneConv %. "public_keys") + void $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle + createOne2OneSubConv def one2OneConvId bob1 "conference" (one2OneConv %. "public_keys") -- bob adds his first client to the subconversation - sub' <- getSubConversation bob (one2OneConv %. "conversation") "conference" >>= getJSON 200 + sub' <- getSubConversation bob one2OneConvId "conference" >>= getJSON 200 + subConvId <- objConvId sub' do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) -- now alice joins with her own client void - $ createExternalCommit alice1 Nothing + $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle testLeaveOne2OneSubConv :: One2OneScenario -> Leaver -> App () @@ -60,15 +64,18 @@ testLeaveOne2OneSubConv scenario leaver = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup def alice1 one2OneConv + void $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle -- create and join subconversation - createOne2OneSubConv alice1 "conference" (one2OneConv %. "public_keys") - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + createOne2OneSubConv def one2OneConvId alice1 "conference" (one2OneConv %. "public_keys") + subConvId <- getSubConvId bob one2OneConvId "conference" + + void $ createExternalCommit subConvId bob1 Nothing >>= sendAndConsumeCommitBundle -- one of the two clients leaves let (leaverClient, leaverIndex, remainingClient) = case leaver of @@ -76,14 +83,13 @@ testLeaveOne2OneSubConv scenario leaver = do Bob -> (bob1, 1, alice1) withWebSocket remainingClient $ \ws -> do - leaveCurrentConv leaverClient - - msg <- consumeMessage remainingClient Nothing ws + leaveConv subConvId leaverClient + msg <- consumeMessage subConvId def remainingClient Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leaverIndex msg %. "message.content.sender.External" `shouldMatchInt` 0 -- the other client commits the pending proposal - void $ createPendingProposalCommit remainingClient >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId remainingClient >>= sendAndConsumeCommitBundle testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do @@ -91,38 +97,39 @@ testDeleteParentOfSubConv secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1, bob1] - (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + traverse_ (uploadNewKeyPackage def) [alice1, bob1] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle -- bob creates a subconversation and adds his own client - createSubConv bob1 "conference" + createSubConv def convId bob1 "conference" + subConvId <- getSubConvId bob convId "conference" -- alice joins with her own client - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle -- bob sends a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, alice" + mp <- createApplicationMessage subConvId bob1 "hello, alice" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 -- alice sends a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, bob" + mp <- createApplicationMessage subConvId bob1 "hello, bob" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 -- alice deletes main conversation withWebSocket bob $ \ws -> do - void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do + void . bindResponse (deleteTeamConv tid (convIdToQidObject convId) alice) $ \resp -> do resp.status `shouldMatchInt` 200 void $ awaitMatch isConvDeleteNotif ws -- bob fails to send a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, alice" + mp <- createApplicationMessage subConvId bob1 "hello, alice" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 404 case secondDomain of @@ -131,7 +138,7 @@ testDeleteParentOfSubConv secondDomain = do -- alice fails to send a message to the subconversation do - mp <- createApplicationMessage alice1 "hello, bob" + mp <- createApplicationMessage subConvId alice1 "hello, bob" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "no-conversation" @@ -140,21 +147,21 @@ testDeleteSubConversation :: (HasCallStack) => Domain -> App () testDeleteSubConversation otherDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] charlie <- randomUser OwnDomain def - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference1" - sub1 <- getSubConversation alice qcnv "conference1" >>= getJSON 200 + createSubConv def convId alice1 "conference1" + sub1 <- getSubConversation alice convId "conference1" >>= getJSON 200 void $ deleteSubConversation charlie sub1 >>= getBody 403 void $ deleteSubConversation alice sub1 >>= getBody 200 - createSubConv alice1 "conference2" - sub2 <- getSubConversation alice qcnv "conference2" >>= getJSON 200 + createSubConv def convId alice1 "conference2" + sub2 <- getSubConversation alice convId "conference2" >>= getJSON 200 void $ deleteSubConversation bob sub2 >>= getBody 200 - sub2' <- getSubConversation alice1 qcnv "conference2" >>= getJSON 200 + sub2' <- getSubConversation alice1 convId "conference2" >>= getJSON 200 sub2 `shouldNotMatch` sub2' data Leaver = Alice | Bob @@ -163,18 +170,19 @@ data Leaver = Alice | Bob testLeaveSubConv :: (HasCallStack) => Leaver -> App () testLeaveSubConv leaver = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] - clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def) [alice, bob, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] - void $ createNewGroup alice1 + clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def def) [alice, bob, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, bob2, charlie1] + convId <- createNewGroup def alice1 withWebSockets [bob, charlie] $ \wss -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss - createSubConv bob1 "conference" - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle + createSubConv def convId bob1 "conference" + subConvId <- getSubConvId bob convId "conference" + void $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId charlie1 Nothing >>= sendAndConsumeCommitBundle -- a member leaves the subconversation let (firstLeaver, idxFirstLeaver) = case leaver of @@ -184,150 +192,163 @@ testLeaveSubConv leaver = do let others = filter (/= firstLeaver) clients withWebSockets others $ \wss -> do - leaveCurrentConv firstLeaver + leaveConv subConvId firstLeaver for_ (zip others wss) $ \(cid, ws) -> do - msg <- consumeMessage cid Nothing ws + msg <- consumeMessage subConvId def cid Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxFirstLeaver msg %. "message.content.sender.External" `shouldMatchInt` 0 withWebSockets (tail others) $ \wss -> do -- a member commits the pending proposal - void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId (head others) >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isNewMLSMessageNotif) wss -- send an application message - void $ createApplicationMessage (head others) "good riddance" >>= sendAndConsumeMessage + void $ createApplicationMessage subConvId (head others) "good riddance" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- check that only 3 clients are left in the subconv do - conv <- getCurrentConv (head others) + conv <- getConv subConvId (head others) mems <- conv %. "members" & asList length mems `shouldMatchInt` 3 -- charlie1 leaves let others' = filter (/= charlie1) others withWebSockets others' $ \wss -> do - leaveCurrentConv charlie1 + leaveConv subConvId charlie1 for_ (zip others' wss) $ \(cid, ws) -> do - msg <- consumeMessage cid Nothing ws + msg <- consumeMessage subConvId def cid Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxCharlie1 msg %. "message.content.sender.External" `shouldMatchInt` 0 -- a member commits the pending proposal - void $ createPendingProposalCommit (head others') >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId (head others') >>= sendAndConsumeCommitBundle -- check that only 2 clients are left in the subconv do - conv <- getCurrentConv (head others) + conv <- getConv subConvId (head others) mems <- conv %. "members" & asList length mems `shouldMatchInt` 2 testCreatorRemovesUserFromParent :: App () testCreatorRemovesUserFromParent = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] - [alice1, bob1, bob2, charlie1, charlie2] <- traverse (createMLSClient def) [alice, bob, bob, charlie, charlie] - traverse_ uploadNewKeyPackage [bob1, bob2, charlie1, charlie2] - (_, qcnv) <- createNewGroup alice1 - - _ <- createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle - - -- save the state of the parent group - parentState <- getMLSState - -- switch to the subgroup - let subConvName = "conference" - createSubConv alice1 subConvName - - for_ [bob1, bob2, charlie1, charlie2] \c -> - createExternalCommit c Nothing >>= sendAndConsumeCommitBundle - -- save the state of the subgroup and switch to the parent context - childState <- getMLSState <* setMLSState parentState - withWebSockets [alice1, charlie1, charlie2] \wss -> do - removeCommitEvents <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle - modifyMLSState $ \s -> s {members = s.members Set.\\ Set.fromList [bob1, bob2]} - - removeCommitEvents %. "events.0.type" `shouldMatch` "conversation.member-leave" - removeCommitEvents %. "events.0.data.reason" `shouldMatch` "removed" - removeCommitEvents %. "events.0.from" `shouldMatch` alice1.user - - for_ wss \ws -> do - n <- awaitMatch isConvLeaveNotif ws - n %. "payload.0.data.reason" `shouldMatch` "removed" - n %. "payload.0.from" `shouldMatch` alice1.user - - setMLSState childState - let idxBob1 :: Int = 1 - idxBob2 :: Int = 2 - for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do - msg <- - awaitMatch - do - \n -> - isJust <$> runMaybeT do - msg <- lift $ n %. "payload.0.data" & asByteString >>= showMessage alice1 - guard =<< lift do - isNewMLSMessageNotif n - - prop <- - maybe mzero pure =<< lift do - lookupField msg "message.content.body.Proposal" - - lift do - (== idx) <$> (prop %. "Remove.removed" & asInt) - ws - for_ ws.client $ \consumer -> - msg %. "payload.0.data" & asByteString >>= mlsCliConsume consumer - - -- remove bob from the child state - modifyMLSState $ \s -> s {members = s.members Set.\\ Set.fromList [bob1, bob2]} - - _ <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - - getSubConversation bob qcnv subConvName >>= flip withResponse \resp -> - assertBool "access to the conversation for bob should be denied" (resp.status == 403) - - for_ [charlie, alice] \m -> do - resp <- getSubConversation m qcnv subConvName - assertBool "alice and charlie should have access to the conversation" (resp.status == 200) - mems <- resp.jsonBody %. "members" & asList - mems `shouldMatchSet` ((renameField "id" "user_id" <=< make) `traverse` [alice1, charlie1, charlie2]) + addUsersToFailureContext [("alice", alice), ("bob", bob), ("charlie", charlie)] $ do + [alice1, bob1, bob2, charlie1, charlie2] <- traverse (createMLSClient def def) [alice, bob, bob, charlie, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, bob2, charlie1, charlie2] + convId <- createNewGroup def alice1 + + _ <- createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle + + -- save the state of the parent group + let subConvName = "conference" + createSubConv def convId alice1 subConvName + subConvId <- getSubConvId alice convId "conference" + + for_ [bob1, bob2, charlie1, charlie2] \c -> + createExternalCommit subConvId c Nothing >>= sendAndConsumeCommitBundle + + withWebSockets [alice1, charlie1, charlie2] \wss -> do + removeCommitEvents <- createRemoveCommit alice1 convId [bob1, bob2] >>= sendAndConsumeCommitBundle + modifyMLSState $ \s -> + s + { convs = + Map.adjust + (\conv -> conv {members = conv.members Set.\\ Set.fromList [bob1, bob2]}) + convId + s.convs + } + + removeCommitEvents %. "events.0.type" `shouldMatch` "conversation.member-leave" + removeCommitEvents %. "events.0.data.reason" `shouldMatch` "removed" + removeCommitEvents %. "events.0.from" `shouldMatch` alice1.user + + for_ wss \ws -> do + n <- awaitMatch isConvLeaveNotif ws + n %. "payload.0.data.reason" `shouldMatch` "removed" + n %. "payload.0.from" `shouldMatch` alice1.user + + let idxBob1 :: Int = 1 + idxBob2 :: Int = 2 + for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do + msg <- + awaitMatch + do + \n -> + isJust <$> runMaybeT do + msg <- lift $ n %. "payload.0.data" & asByteString >>= showMessage def alice1 + guard =<< lift do + isNewMLSMessageNotif n + + prop <- + maybe mzero pure =<< lift do + lookupField msg "message.content.body.Proposal" + + lift do + (== idx) <$> (prop %. "Remove.removed" & asInt) + ws + for_ ws.client $ \consumer -> + msg %. "payload.0.data" & asByteString >>= mlsCliConsume subConvId def consumer + + -- remove bob from the child state + modifyMLSState $ \s -> + s + { convs = + Map.adjust + (\conv -> conv {members = conv.members Set.\\ Set.fromList [bob1, bob2]}) + subConvId + s.convs + } + + _ <- createPendingProposalCommit subConvId alice1 >>= sendAndConsumeCommitBundle + + getSubConversation bob convId subConvName >>= flip withResponse \resp -> + assertBool "access to the conversation for bob should be denied" (resp.status == 403) + + for_ [charlie, alice] \m -> do + resp <- getSubConversation m convId subConvName + assertBool "alice and charlie should have access to the conversation" (resp.status == 200) + mems <- resp.jsonBody %. "members" & asList + mems `shouldMatchSet` ((renameField "id" "user_id" <=< make) `traverse` [alice1, charlie1, charlie2]) testResendingProposals :: (HasCallStack) => App () testResendingProposals = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] [alice1, alice2, bob1, bob2, bob3, charlie1] <- traverse - (createMLSClient def) + (createMLSClient def def) [alice, alice, bob, bob, bob, charlie] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2, bob3, charlie1] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2, bob3, charlie1] - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob, charlie] >>= sendAndConsumeCommitBundle + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [alice, bob, charlie] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference" + createSubConv def conv alice1 "conference" + subConvId <- getSubConvId alice conv "conference" - void $ createExternalCommit alice2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob3 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId alice2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob3 Nothing >>= sendAndConsumeCommitBundle - leaveCurrentConv bob1 - leaveCurrentConv bob2 - leaveCurrentConv bob3 + leaveConv subConvId bob1 + leaveConv subConvId bob2 + leaveConv subConvId bob3 - mls <- getMLSState - withWebSockets (charlie1 : toList mls.members) \wss -> do - void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle + subConv <- getMLSConv subConvId + withWebSockets (charlie1 : toList subConv.members) \wss -> do + void $ createExternalCommit subConvId charlie1 Nothing >>= sendAndConsumeCommitBundle -- consume proposals after backend resends them for_ wss \ws -> do replicateM 3 do - msg <- consumeMessage (fromJust ws.client) Nothing ws + msg <- consumeMessage subConvId def (fromJust ws.client) Nothing ws msg %. "message.content.sender.External" `shouldMatchInt` 0 - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId alice1 >>= sendAndConsumeCommitBundle sub <- getSubConversation alice1 conv "conference" >>= getJSON 200 let members = diff --git a/integration/test/Test/MLS/Unreachable.hs b/integration/test/Test/MLS/Unreachable.hs index 4e32d293508..17bd296650a 100644 --- a/integration/test/Test/MLS/Unreachable.hs +++ b/integration/test/Test/MLS/Unreachable.hs @@ -33,13 +33,13 @@ testAddUsersSomeReachable = do otherDomain <- make OtherDomain & asString [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 void $ withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - mp <- createAddCommit alice1 [charlie] + mp <- createAddCommit alice1 convId [charlie] pure (mp, thirdDomain) -- try adding Charlie now that his backend is unreachable @@ -52,24 +52,24 @@ testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => App () testAddUserWithUnreachableRemoteUsers = do resourcePool <- asks resourcePool runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do - (alice1, bob, brad, chris) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do + (alice1, bob, brad, chris, convId) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do [own, other] <- forM [OwnDomain, OtherDomain] $ asString . make [alice, bob, brad, charlie, chris] <- createAndConnectUsers [own, other, other, cDom.berDomain, cDom.berDomain] [alice1, charlie1, chris1] <- - traverse (createMLSClient def) [alice, charlie, chris] - traverse_ uploadNewKeyPackage [charlie1, chris1] - void $ createNewGroup alice1 + traverse (createMLSClient def def) [alice, charlie, chris] + traverse_ (uploadNewKeyPackage def) [charlie1, chris1] + convId <- createNewGroup def alice1 void $ withWebSocket charlie $ \ws -> do - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - pure (alice1, bob, brad, chris) + pure (alice1, bob, brad, chris, convId) - [bob1, brad1] <- traverse (createMLSClient def) [bob, brad] - traverse_ uploadNewKeyPackage [bob1, brad1] + [bob1, brad1] <- traverse (createMLSClient def def) [bob, brad] + traverse_ (uploadNewKeyPackage def) [bob1, brad1] do - mp <- createAddCommit alice1 [bob] + mp <- createAddCommit alice1 convId [bob] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] @@ -78,12 +78,12 @@ testAddUserWithUnreachableRemoteUsers = do void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getBody 201 do - mp <- createAddCommit alice1 [brad] + mp <- createAddCommit alice1 convId [brad] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getBody 201 do mp <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> - createAddCommit alice1 [chris] + createAddCommit alice1 convId [chris] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] @@ -98,13 +98,13 @@ testAddUnreachableUserFromFederatingBackend = do [alice, bob, charlie, chad] <- createAndConnectUsers [ownDomain, otherDomain, cDom.berDomain, cDom.berDomain] - [alice1, bob1, charlie1, chad1] <- traverse (createMLSClient def) [alice, bob, charlie, chad] - traverse_ uploadNewKeyPackage [bob1, charlie1, chad1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1, chad1] <- traverse (createMLSClient def def) [alice, bob, charlie, chad] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1, chad1] + convId <- createNewGroup def alice1 withWebSockets [bob, charlie] $ \wss -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle forM_ wss $ awaitMatch isMemberJoinNotif - createAddCommit alice1 [chad] + createAddCommit alice1 convId [chad] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 2eecee9d686..66caf35d070 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -12,14 +12,14 @@ import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception import GHC.Generics (Generic) -import GHC.Stack (HasCallStack) +import GHC.Stack (HasCallStack, callStack) import System.FilePath import Testlib.JSON import Testlib.Types import Prelude failApp :: (HasCallStack) => String -> App a -failApp msg = throw (AppFailure msg) +failApp msg = throw (AppFailure msg callStack) getPrekey :: App Value getPrekey = App $ do diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 28ddf0c0af1..f9d00023054 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -261,6 +261,15 @@ printFailureDetails (AssertionFailure stack mbResponse ctx msg) = do : toList (fmap prettyResponse mbResponse) <> toList (fmap prettyContext ctx) +printAppFailureDetails :: AppFailure -> IO String +printAppFailureDetails (AppFailure msg stack) = do + s <- prettierCallStack stack + pure . unlines $ + colored yellow "app failure:" + : colored red msg + : "\n" + : [s] + prettyContext :: String -> String prettyContext ctx = do unlines diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index b5611178b6f..f981f64dd52 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -5,7 +5,6 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class import Control.Monad.Reader -import Data.Default import Data.Function ((&)) import Data.Functor import Data.IORef @@ -14,6 +13,7 @@ import Data.Maybe (fromMaybe) import Data.Traversable (for) import qualified Data.Yaml as Yaml import qualified Database.CQL.IO as Cassandra +import GHC.Stack import qualified Network.HTTP.Client as HTTP import qualified OpenSSL.Session as OpenSSL import System.Directory @@ -171,15 +171,17 @@ mkMLSState = Codensity $ \k -> k MLSState { baseDir = tmp, - members = mempty, - newMembers = mempty, - groupId = Nothing, - convId = Nothing, - clientGroupState = mempty, - epoch = 0, - ciphersuite = def, - protocol = MLSProtocolMLS + convs = mempty, + clientGroupState = mempty } +getMLSConv :: (HasCallStack) => ConvId -> App MLSConv +getMLSConv convId = do + mConv <- Map.lookup convId . (.convs) <$> getMLSState + case mConv of + Just conv -> pure conv + Nothing -> do + assertFailure $ "MLSConv not found, convId=" <> show convId + withAPIVersion :: Int -> App a -> App a withAPIVersion v = local $ \e -> e {defaultAPIVersion = v} diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 96ee6da2492..10b0ff27420 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -436,16 +436,16 @@ objSubConv x = do lift $ asString sub' pure (obj, sub) --- | Turn an object parseable by 'objSubConv' into a canonical flat representation. -objSubConvObject :: (HasCallStack, MakesValue a) => a -> App Value -objSubConvObject x = do - (convId, mSubConvId) <- objSubConv x - (domain, id_) <- objQid convId - pure . object $ - [ "domain" .= domain, - "id" .= id_ - ] - <> ["subconv_id" .= sub | sub <- toList mSubConvId] +objConvId :: (HasCallStack, MakesValue conv) => conv -> App ConvId +objConvId conv = do + v <- make conv + -- Domain and ConvId either come from parent_qualified_id or qualified_id + mParent <- lookupField v "parent_qualified_id" + (domain, id_) <- objQid $ fromMaybe v mParent + + groupId <- traverse asString =<< asOptional (lookupField v "group_id") + subconvId <- traverse asString =<< asOptional (lookupField v "subconv_id") + pure ConvId {..} instance MakesValue ClientIdentity where make cid = diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 379547c4d2b..4e9dae0984d 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -11,7 +11,7 @@ where import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as E -import Control.Monad.Catch (catch, throwM) +import Control.Monad.Catch (catch, displayException, throwM) import Control.Monad.Codensity import Control.Monad.Extra import Control.Monad.Reader @@ -38,6 +38,7 @@ import System.IO.Temp (createTempDirectory, writeTempFile) import System.Posix (keyboardSignal, killProcess, signalProcess) import System.Process import Testlib.App +import Testlib.Assertions (prettierCallStack) import Testlib.HTTP import Testlib.JSON import Testlib.Printing @@ -118,20 +119,19 @@ traverseConcurrentlyCodensity f args = do pure result startDynamicBackends :: [ServiceOverrides] -> ([String] -> App a) -> App a -startDynamicBackends beOverrides k = - runCodensity - do - when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." - pool <- asks (.resourcePool) - resources <- acquireResources (Prelude.length beOverrides) pool - void $ - traverseConcurrentlyCodensity - (void . uncurry startDynamicBackend) - (zip resources beOverrides) - pure $ map (.berDomain) resources - k - -startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App String +startDynamicBackends beOverrides k = do + let startDynamicBackendsCodensity = do + when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." + pool <- asks (.resourcePool) + resources <- acquireResources (Prelude.length beOverrides) pool + void $ + traverseConcurrentlyCodensity + (void . uncurry startDynamicBackend) + (zip resources beOverrides) + pure $ map (.berDomain) resources + runCodensity startDynamicBackendsCodensity k + +startDynamicBackend :: (HasCallStack) => BackendResource -> ServiceOverrides -> Codensity App String startDynamicBackend resource beOverrides = do let overrides = mconcat @@ -261,10 +261,11 @@ startBackend resource overrides = do traverseConcurrentlyCodensity (withProcess resource overrides) allServices lift $ ensureBackendReachable resource.berDomain -ensureBackendReachable :: String -> App () +ensureBackendReachable :: (HasCallStack) => String -> App () ensureBackendReachable domain = do env <- ask - let checkServiceIsUpReq = do + let checkServiceIsUpReq :: (HasCallStack) => App Bool + checkServiceIsUpReq = do req <- rawBaseRequest env.domain1 @@ -314,9 +315,12 @@ data ServiceInstance = ServiceInstance timeout :: Int -> IO a -> IO (Maybe a) timeout usecs action = either (const Nothing) Just <$> race (threadDelay usecs) action -cleanupService :: ServiceInstance -> IO () +cleanupService :: (HasCallStack) => ServiceInstance -> IO () cleanupService inst = do - let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () + let ignoreExceptions :: (HasCallStack) => IO () -> IO () + ignoreExceptions action = E.catch action $ \(e :: E.SomeException) -> do + callstackPretty <- prettierCallStack callStack + putStrLn $ colored red $ "Exception while cleaning up a service: " <> displayException e <> "\ncallstack: \n" <> callstackPretty ignoreExceptions $ do mPid <- getPid inst.handle for_ mPid (signalProcess keyboardSignal) @@ -329,7 +333,7 @@ cleanupService inst = do whenM (doesDirectoryExist inst.config) $ removeDirectoryRecursive inst.config -- | Wait for a service to come up. -waitUntilServiceIsUp :: String -> Service -> App () +waitUntilServiceIsUp :: (HasCallStack) => String -> Service -> App () waitUntilServiceIsUp domain srv = retryRequestUntil (checkServiceIsUp domain srv) @@ -346,7 +350,7 @@ checkServiceIsUp domain srv = do eith <- liftIO (E.try checkStatus) pure $ either (\(_e :: HTTP.HttpException) -> False) id eith -withProcess :: BackendResource -> ServiceOverrides -> Service -> Codensity App () +withProcess :: (HasCallStack) => BackendResource -> ServiceOverrides -> Service -> Codensity App () withProcess resource overrides service = do let domain = berDomain resource sm <- lift $ getServiceMap domain @@ -393,7 +397,7 @@ logToConsole colorize prefix hdl = do `E.catch` (\(_ :: E.IOException) -> pure ()) go -retryRequestUntil :: (HasCallStack) => App Bool -> String -> App () +retryRequestUntil :: (HasCallStack) => ((HasCallStack) => App Bool) -> String -> App () retryRequestUntil reqAction err = do isUp <- retrying diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index d5385a16376..29a501c03da 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -37,6 +37,8 @@ runTest ge action = lowerCodensity $ do E.throw e, E.Handler -- AssertionFailure (fmap Left . printFailureDetails), + E.Handler -- AppFailure + (fmap Left . printAppFailureDetails), E.Handler (fmap Left . printExceptionDetails) ] diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index e25b33d06f8..0f91d48c595 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -252,7 +252,7 @@ instance Default Ciphersuite where def = Ciphersuite "0x0001" data ClientGroupState = ClientGroupState - { group :: Maybe ByteString, + { groups :: Map ConvId ByteString, -- | mls-test-cli stores by signature scheme keystore :: Map String ByteString, credType :: CredentialType @@ -262,7 +262,7 @@ data ClientGroupState = ClientGroupState instance Default ClientGroupState where def = ClientGroupState - { group = Nothing, + { groups = mempty, keystore = mempty, credType = BasicCredentialType } @@ -277,17 +277,32 @@ csSignatureScheme (Ciphersuite code) = case code of data MLSProtocol = MLSProtocolMLS | MLSProtocolMixed deriving (Eq, Show) +data ConvId = ConvId + { domain :: String, + id_ :: String, + groupId :: Maybe String, + subconvId :: Maybe String + } + deriving (Show, Eq, Ord) + +convIdToQidObject :: ConvId -> Value +convIdToQidObject convId = object [fromString "id" .= convId.id_, fromString "domain" .= convId.domain] + data MLSState = MLSState { baseDir :: FilePath, - members :: Set ClientIdentity, + convs :: Map ConvId MLSConv, + clientGroupState :: Map ClientIdentity ClientGroupState + } + deriving (Show) + +data MLSConv = MLSConv + { members :: Set ClientIdentity, -- | users expected to receive a welcome message after the next commit newMembers :: Set ClientIdentity, - groupId :: Maybe String, - convId :: Maybe Value, - clientGroupState :: Map ClientIdentity ClientGroupState, + groupId :: String, + convId :: ConvId, epoch :: Word64, - ciphersuite :: Ciphersuite, - protocol :: MLSProtocol + ciphersuite :: Ciphersuite } deriving (Show) @@ -377,13 +392,13 @@ modifyMLSState f = do getBaseDir :: App FilePath getBaseDir = fmap (.baseDir) getMLSState -data AppFailure = AppFailure String +data AppFailure = AppFailure String CallStack instance Show AppFailure where - show (AppFailure msg) = msg + show (AppFailure msg _) = msg instance Exception AppFailure where - displayException (AppFailure msg) = msg + displayException (AppFailure msg _) = msg instance MonadFail App where fail msg = assertFailure ("Pattern matching failure: " <> msg)