diff --git a/changelog.d/3-bug-fixes/remote-member-removal-notification b/changelog.d/3-bug-fixes/remote-member-removal-notification new file mode 100644 index 0000000000..a94c916a68 --- /dev/null +++ b/changelog.d/3-bug-fixes/remote-member-removal-notification @@ -0,0 +1 @@ +This fixes a bug where a remote member is removed from a conversation while their backend is unreachable, and the backend does not receive the removal notification once it is reachable again. diff --git a/integration/default.nix b/integration/default.nix index 8019fdd1f8..b42304134c 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -19,6 +19,7 @@ , cql-io , cryptonite , data-default +, data-timeout , directory , errors , exceptions @@ -86,6 +87,7 @@ mkDerivation { cql-io cryptonite data-default + data-timeout directory errors exceptions diff --git a/integration/integration.cabal b/integration/integration.cabal index 7e989f5a98..5ff99f8994 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -100,6 +100,7 @@ library Notifications RunAllTests SetupHelpers + Test.AccessUpdate Test.AssetDownload Test.B2B Test.Brig @@ -108,8 +109,10 @@ library Test.Demo Test.Federation Test.Federator + Test.MessageTimer Test.Notifications Test.Presence + Test.Roles Test.User Testlib.App Testlib.Assertions @@ -146,6 +149,7 @@ library , cql-io , cryptonite , data-default + , data-timeout , directory , errors , exceptions diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 967786f86c..6c1791bec5 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -5,6 +5,7 @@ module API.Galley where import Control.Lens hiding ((.=)) import Control.Monad.Reader import Data.Aeson qualified as Aeson +import Data.Aeson.Types qualified as Aeson import Data.ByteString.Lazy qualified as LBS import Data.ProtoLens qualified as Proto import Data.ProtoLens.Labels () @@ -73,6 +74,21 @@ postConversation user cc = do ccv <- make cc submit "POST" $ req & addJSON ccv +deleteTeamConversation :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + String -> + conv -> + user -> + App Response +deleteTeamConversation tid qcnv user = do + cnv <- snd <$> objQid qcnv + let path = joinHttpPath ["teams", tid, "conversations", cnv] + req <- baseRequest user Galley Versioned path + submit "DELETE" req + putConversationProtocol :: ( HasCallStack, MakesValue user, @@ -210,12 +226,39 @@ getGroupInfo user conv = do req <- baseRequest user Galley Versioned path submit "GET" req -addMembers :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> [Value] -> App Response -addMembers usr qcnv newMembers = do +data AddMembers = AddMembers + { users :: [Value], + role :: Maybe String, + version :: Maybe Int + } + +instance Default AddMembers where + def = AddMembers {users = [], role = Nothing, version = Nothing} + +addMembers :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + AddMembers -> + App Response +addMembers usr qcnv opts = do (convDomain, convId) <- objQid qcnv - qUsers <- mapM objQidObject newMembers - req <- baseRequest usr Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members"]) - submit "POST" (req & addJSONObject ["qualified_users" .= qUsers]) + qUsers <- mapM objQidObject opts.users + let path = case opts.version of + Just v | v <= 1 -> ["conversations", convId, "members", "v2"] + _ -> ["conversations", convDomain, convId, "members"] + req <- + baseRequest + usr + Galley + (maybe Versioned ExplicitVersion opts.version) + (joinHttpPath path) + submit "POST" $ + req + & addJSONObject + ( ["qualified_users" .= qUsers] + <> ["conversation_role" .= r | r <- toList opts.role] + ) removeMember :: (HasCallStack, MakesValue remover, MakesValue conv, MakesValue removed) => remover -> conv -> removed -> App Response removeMember remover qcnv removed = do @@ -223,3 +266,122 @@ removeMember remover qcnv removed = do (removedDomain, removedId) <- objQid removed req <- baseRequest remover Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members", removedDomain, removedId]) submit "DELETE" req + +postConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + Maybe String -> + App Response +postConversationCode user conv mbpassword mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "POST" + ( req + & addJSONObject ["password" .= pw | pw <- maybeToList mbpassword] + & maybe id zHost mbZHost + ) + +getConversationCode :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + Maybe String -> + App Response +getConversationCode user conv mbZHost = do + convId <- objId conv + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId, "code"]) + submit + "GET" + ( req + & addQueryParams [("cnv", convId)] + & maybe id zHost mbZHost + ) + +changeConversationName :: + (HasCallStack, MakesValue user, MakesValue conv, MakesValue name) => + user -> + conv -> + name -> + App Response +changeConversationName user qcnv name = do + (convDomain, convId) <- objQid qcnv + let path = joinHttpPath ["conversations", convDomain, convId, "name"] + nameReq <- make name + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["name" .= nameReq]) + +updateRole :: + ( HasCallStack, + MakesValue callerUser, + MakesValue targetUser, + MakesValue roleUpdate, + MakesValue qcnv + ) => + callerUser -> + targetUser -> + roleUpdate -> + qcnv -> + App Response +updateRole caller target role qcnv = do + (cnvDomain, cnvId) <- objQid qcnv + (tarDomain, tarId) <- objQid target + roleReq <- make role + req <- + baseRequest + caller + Galley + Versioned + ( joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId] + ) + submit "PUT" (req & addJSONObject ["conversation_role" .= roleReq]) + +updateReceiptMode :: + ( HasCallStack, + MakesValue user, + MakesValue conv, + MakesValue mode + ) => + user -> + conv -> + mode -> + App Response +updateReceiptMode user qcnv mode = do + (cnvDomain, cnvId) <- objQid qcnv + modeReq <- make mode + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "receipt-mode"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["receipt_mode" .= modeReq]) + +updateAccess :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + [Aeson.Pair] -> + App Response +updateAccess user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "access"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject update) + +updateMessageTimer :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + Word64 -> + App Response +updateMessageTimer user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + updateReq <- make update + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "message-timer"] + req <- baseRequest user Galley Versioned path + submit "PUT" (addJSONObject ["message_timer" .= updateReq] req) diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 0364bee7ce..a6ffb6505b 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -65,8 +65,63 @@ isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" isConvLeaveNotif :: MakesValue a => a -> App Bool isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave" -isNotifConv :: (MakesValue conv, MakesValue a) => conv -> a -> App Bool +isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) -isNotifForUser :: (MakesValue user, MakesValue a) => user -> a -> App Bool +isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) + +isNotifFromUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool +isNotifFromUser user n = fieldEquals n "payload.0.qualified_from" (objQidObject user) + +isConvNameChangeNotif :: (HasCallStack, MakesValue a) => a -> App Bool +isConvNameChangeNotif n = fieldEquals n "payload.0.type" "conversation.rename" + +isMemberUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isMemberUpdateNotif n = fieldEquals n "payload.0.type" "conversation.member-update" + +isReceiptModeUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isReceiptModeUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.receipt-mode-update" + +isConvMsgTimerUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvMsgTimerUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.message-timer-update" + +isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvAccessUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.access-update" + +isConvCreateNotif :: MakesValue a => a -> App Bool +isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create" + +isConvDeleteNotif :: MakesValue a => a -> App Bool +isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" + +assertLeaveNotification :: + ( HasCallStack, + MakesValue fromUser, + MakesValue conv, + MakesValue user, + MakesValue kickedUser + ) => + fromUser -> + conv -> + user -> + String -> + kickedUser -> + App () +assertLeaveNotification fromUser conv user client leaver = + void $ + awaitNotification + user + client + noValue + 2 + ( allPreds + [ isConvLeaveNotif, + isNotifConv conv, + isNotifForUser leaver, + isNotifFromUser fromUser + ] + ) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index cef03af91b..c48749621d 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -4,6 +4,7 @@ module SetupHelpers where import API.Brig qualified as Brig import API.BrigInternal qualified as Internal +import API.Common import API.Galley import Control.Concurrent (threadDelay) import Control.Monad.Reader @@ -31,15 +32,43 @@ deleteUser user = bindResponse (Brig.deleteUser user) $ \resp -> do resp.status `shouldMatchInt` 200 -- | returns (user, team id) -createTeam :: (HasCallStack, MakesValue domain) => domain -> App (Value, String) -createTeam domain = do +createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value]) +createTeam domain memberCount = do res <- Internal.createUser domain def {Internal.team = True} - user <- res.json - tid <- user %. "team" & asString - -- TODO - -- SQS.assertTeamActivate "create team" tid - -- refreshIndex - pure (user, tid) + owner <- res.json + tid <- owner %. "team" & asString + members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid + pure (owner, tid, members) + +createTeamMember :: + (HasCallStack, MakesValue inviter) => + inviter -> + String -> + App Value +createTeamMember inviter tid = do + newUserEmail <- randomEmail + let invitationJSON = ["role" .= "member", "email" .= newUserEmail] + invitationReq <- + baseRequest inviter Brig Versioned $ + joinHttpPath ["teams", tid, "invitations"] + invitation <- getJSON 201 =<< submit "POST" (addJSONObject invitationJSON invitationReq) + invitationId <- objId invitation + invitationCodeReq <- + rawBaseRequest inviter Brig Unversioned "/i/teams/invitation-code" + <&> addQueryParams [("team", tid), ("invitation_id", invitationId)] + invitationCode <- bindResponse (submit "GET" invitationCodeReq) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "code" & asString + let registerJSON = + [ "name" .= newUserEmail, + "email" .= newUserEmail, + "password" .= defPassword, + "team_code" .= invitationCode + ] + registerReq <- + rawBaseRequest inviter Brig Versioned "/register" + <&> addJSONObject registerJSON + getJSON 201 =<< submit "POST" registerReq connectUsers :: ( HasCallStack, diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs new file mode 100644 index 0000000000..7202717a8a --- /dev/null +++ b/integration/test/Test/AccessUpdate.hs @@ -0,0 +1,122 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.AccessUpdate where + +import API.Brig +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +-- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- The test asserts that, among others, remote users are removed from a +-- conversation when an access update occurs that disallows guests from +-- accessing. +testAccessUpdateGuestRemoved :: HasCallStack => App () +testAccessUpdateGuestRemoved = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + dee <- randomUser OtherDomain def + mapM_ (connectUsers alice) [charlie, dee] + [aliceClient, bobClient, charlieClient, deeClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie, dee] + conv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + >>= getJSON 201 + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv dee deeClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +-- @END + +testAccessUpdateGuestRemovedUnreachableRemotes :: HasCallStack => App () +testAccessUpdateGuestRemovedUnreachableRemotes = do + resourcePool <- asks resourcePool + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + connectUsers alice charlie + [aliceClient, bobClient, charlieClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie] + (conv, dee) <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + dee <- randomUser dynBackend.berDomain def + connectUsers alice dee + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + ) + >>= getJSON 201 + pure (conv, dee) + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +testAccessUpdateWithRemotes :: HasCallStack => App () +testAccessUpdateWithRemotes = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + let update_access_value = ["code"] + update_access_role_value = ["team_member", "non_team_member", "guest", "service"] + update = ["access" .= update_access_value, "access_role" .= update_access_role_value] + withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateAccess alice conv update >>= getJSON 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvAccessUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.access" `shouldMatch` update_access_value + notif %. "payload.0.data.access_role_v2" `shouldMatch` update_access_role_value diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index d994a857b7..394d9642c6 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -1,19 +1,40 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + module Test.Conversation where -import API.Brig (getConnections, postConnection) -import API.BrigInternal as Internal +import API.Brig +import API.BrigInternal import API.Galley import API.GalleyInternal import Control.Applicative import Control.Concurrent (threadDelay) +import Control.Monad.Codensity +import Control.Monad.Reader import Data.Aeson qualified as Aeson import GHC.Stack -import SetupHelpers +import Notifications +import SetupHelpers hiding (deleteUser) import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude +import Testlib.ResourcePool testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do @@ -183,7 +204,7 @@ testAddMembersFullyConnectedProteus = do cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 200 users <- resp.json %. "data.users" >>= asList addedUsers <- forM users (%. "qualified_id") @@ -209,10 +230,66 @@ testAddMembersNonFullyConnectedProteus = do cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] +testAddMember :: HasCallStack => App () +testAddMember = do + alice <- randomUser OwnDomain def + aliceId <- alice %. "qualified_id" + -- create conversation with no users + cid <- postConversation alice defProteus >>= getJSON 201 + bob <- randomUser OwnDomain def + bobId <- bob %. "qualified_id" + let addMember = addMembers alice cid def {role = Just "wire_member", users = [bobId]} + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "not-connected" + connectUsers alice bob + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject cid + users <- resp.json %. "data.users" >>= asList + addedUsers <- forM users (%. "qualified_id") + addedUsers `shouldMatchSet` [bobId] + + -- check that both users can see the conversation + bindResponse (getConversation alice cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` bobId + mem %. "conversation_role" `shouldMatch` "wire_member" + + bindResponse (getConversation bob cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` aliceId + mem %. "conversation_role" `shouldMatch` "wire_admin" + +testAddMemberV1 :: HasCallStack => Domain -> App () +testAddMemberV1 domain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, domain] + conv <- postConversation alice defProteus >>= getJSON 201 + bobId <- bob %. "qualified_id" + let opts = + def + { version = Just 1, + role = Just "wire_member", + users = [bobId] + } + bindResponse (addMembers alice conv opts) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject conv + users <- resp.json %. "data.users" >>= asList + traverse (%. "qualified_id") users `shouldMatchSet` [bobId] + testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do let overrides = @@ -249,7 +326,7 @@ testAddReachableWithUnreachableRemoteUsers = do pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" - bindResponse (addMembers alex conv [bobId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [bobId]}) $ \resp -> do -- This test is updated to reflect the changes in `performConversationJoin` -- `performConversationJoin` now does a full check between all federation members -- that will be in the conversation when adding users to a conversation. This is @@ -273,7 +350,7 @@ testAddUnreachable = do pure ([alex, charlie], domains, conv) charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alex conv [charlieId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 533 -- All of the domains that are in the conversation, or will be in the conversation, -- need to be reachable so we can check that the graph for those domains is fully connected. @@ -306,7 +383,7 @@ testAddingUserNonFullyConnectedFederation = do bobId <- bob %. "qualified_id" charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alice conv [bobId, charlieId]) $ \resp -> do + bindResponse (addMembers alice conv def {users = [bobId, charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [other, dynBackend] @@ -341,5 +418,264 @@ testAddUserWhenOtherBackendOffline = do let newConv = defProteus {qualifiedUsers = [charlie]} conv <- postConversation alice newConv >>= getJSON 201 pure ([alice, alex], conv) - bindResponse (addMembers alice conv [alex]) $ \resp -> do + bindResponse (addMembers alice conv def {users = [alex]}) $ \resp -> do resp.status `shouldMatchInt` 200 + +testSynchroniseUserRemovalNotification :: HasCallStack => App () +testSynchroniseUserRemovalNotification = do + resourcePool <- asks resourcePool + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (conv, charlie, client) <- + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + charlie <- randomUser dynBackend.berDomain def + client <- objId $ bindResponse (addClient charlie def) $ getJSON 201 + mapM_ (connectUsers charlie) [alice, bob] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + pure (conv, charlie, client) + + let newConvName = "The new conversation name" + bindResponse (changeConversationName alice conv newConvName) $ \resp -> + resp.status `shouldMatchInt` 200 + bindResponse (removeMember alice conv charlie) $ \resp -> + resp.status `shouldMatchInt` 200 + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + nameNotif <- awaitNotification charlie client noValue 2 isConvNameChangeNotif + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + leaveNotif <- awaitNotification charlie client noValue 2 isConvLeaveNotif + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testConvRenaming :: HasCallStack => App () +testConvRenaming = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + let newConvName = "The new conversation name" + withWebSockets [alice, bob] $ \wss -> do + for_ wss $ \ws -> do + void $ changeConversationName alice conv newConvName >>= getBody 200 + nameNotif <- awaitMatch 10 isConvNameChangeNotif ws + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testReceiptModeWithRemotesOk :: HasCallStack => App () +testReceiptModeWithRemotesOk = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testReceiptModeWithRemotesUnreachable :: HasCallStack => App () +testReceiptModeWithRemotesUnreachable = do + ownDomain <- asString OwnDomain + alice <- randomUser ownDomain def + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + bob <- randomUser dynBackend def + connectUsers alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testDeleteLocalMember :: HasCallStack => App () +testDeleteLocalMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv alex) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject alex + -- Now that Alex is gone, try removing her once again + bindResponse (removeMember alice conv alex) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMember :: HasCallStack => App () +testDeleteRemoteMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv bob) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMemberRemoteUnreachable :: HasCallStack => App () +testDeleteRemoteMemberRemoteUnreachable = do + [alice, bob, bart] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + charlie <- randomUser dynBackend def + connectUsers alice charlie + postConversation + alice + (defProteus {qualifiedUsers = [bob, bart, charlie]}) + >>= getJSON 201 + void $ withWebSockets [alice, bob] $ \wss -> do + void $ removeMember alice conv bob >>= getBody 200 + for wss $ \ws -> do + leaveNotif <- awaitMatch 10 isConvLeaveNotif ws + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + leaveNotif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + leaveNotif %. "payload.0.data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteTeamConversationWithRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithRemoteMembers = do + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + bob <- randomUser OtherDomain def + connectUsers alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + + void $ withWebSockets [alice, bob] $ \wss -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + for wss $ \ws -> do + notif <- awaitMatch 10 isConvDeleteNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testDeleteTeamConversationWithUnreachableRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithUnreachableRemoteMembers = do + resourcePool <- asks resourcePool + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + + let assertNotification :: (HasCallStack, MakesValue n) => n -> App () + assertNotification notif = do + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (bob, bobClient) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, dynBackend.berDomain] + sequence_ + [ createFedConn x (FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + + bob <- randomUser dynBackend.berDomain def + bobClient <- objId $ bindResponse (addClient bob def) $ getJSON 201 + connectUsers alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + pure (bob, bobClient) + withWebSocket alice $ \ws -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + notif <- awaitMatch 10 isConvDeleteNotif ws + assertNotification notif + void $ runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + notif <- awaitNotification bob bobClient noValue 2 isConvDeleteNotif + assertNotification notif + +testLeaveConversationSuccess :: HasCallStack => App () +testLeaveConversationSuccess = do + [alice, bob, chad, dee] <- + createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] + [aClient, bClient] <- forM [alice, bob] $ \user -> + objId $ bindResponse (addClient user def) $ getJSON 201 + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + eve <- randomUser dynDomain def + eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 + connectUsers alice eve + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, chad, dee, eve] + } + ) + >>= getJSON 201 + void $ removeMember chad conv chad >>= getBody 200 + assertLeaveNotification chad conv alice aClient chad + assertLeaveNotification chad conv bob bClient chad + assertLeaveNotification chad conv eve eClient chad + +testOnUserDeletedConversations :: HasCallStack => App () +testOnUserDeletedConversations = do + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString + [alice, alex, bob, bart, chad] <- + createAndConnectUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] + bobId <- bob %. "qualified_id" + ooConvId <- do + l <- getAllConvs alice + let isWith users c = do + t <- (==) <$> (c %. "type" & asInt) <*> pure 2 + others <- c %. "members.others" & asList + qIds <- for others (%. "qualified_id") + pure $ qIds == users && t + c <- head <$> filterM (isWith [bobId]) l + c %. "qualified_id" + + mainConvBefore <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob, bart, chad]}) + >>= getJSON 201 + + void $ withWebSocket alex $ \ws -> do + void $ deleteUser bob >>= getBody 200 + n <- awaitMatch 10 isConvLeaveNotif ws + n %. "payload.0.qualified_from" `shouldMatch` bobId + n %. "payload.0.qualified_conversation" `shouldMatch` (mainConvBefore %. "qualified_id") + + do + -- Bob is not in the one-to-one conversation with Alice any more + conv <- getConversation alice ooConvId >>= getJSON 200 + shouldBeEmpty $ conv %. "members.others" + do + -- Bob is not in the main conversation any more + mainConvAfter <- getConversation alice (mainConvBefore %. "qualified_id") >>= getJSON 200 + mems <- mainConvAfter %. "members.others" & asList + memIds <- for mems (%. "qualified_id") + expectedIds <- for [alex, bart, chad] (%. "qualified_id") + memIds `shouldMatchSet` expectedIds + +testUpdateConversationByRemoteAdmin :: HasCallStack => App () +testUpdateConversationByRemoteAdmin = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + void $ updateRole alice bob "wire_admin" (conv %. "qualified_id") >>= getBody 200 + void $ withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateReceiptMode bob conv (41 :: Int) >>= getBody 200 + for_ wss $ \ws -> awaitMatch 10 isReceiptModeUpdateNotif ws diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 3709ea4799..547c659822 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -43,7 +43,7 @@ testModifiedBrig = do testModifiedGalley :: HasCallStack => App () testModifiedGalley = do - (_user, tid) <- createTeam OwnDomain + (_user, tid, _) <- createTeam OwnDomain 1 let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value getFeatureStatus domain team = do @@ -56,7 +56,7 @@ testModifiedGalley = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do - (_user, tid') <- createTeam domain + (_user, tid', _) <- createTeam domain 1 getFeatureStatus domain tid' `shouldMatch` "enabled" testModifiedCannon :: HasCallStack => App () @@ -84,7 +84,7 @@ testModifiedServices = do } withModifiedBackend serviceMap $ \domain -> do - (_user, tid) <- createTeam domain + (_user, tid, _) <- createTeam domain 1 bindResponse (Internal.getTeamFeature domain "searchVisibility" tid) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 5a70f3ad1f..97ec011f02 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -4,6 +4,7 @@ module Test.Federation where import API.Brig qualified as API +import API.BrigInternal qualified as API import API.Galley import Control.Lens import Control.Monad.Codensity @@ -27,10 +28,22 @@ testNotificationsForOfflineBackends = do otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 otherClient2 <- objId $ bindResponse (API.addClient otherUser2 def) $ getJSON 201 - -- We call it 'downBackend' because it is down for the most of this test + -- We call it 'downBackend' because it is down for most of this test -- except for setup and assertions. Perhaps there is a better name. runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, downBackend.berDomain] + sequence_ + [ API.createFedConn x (API.FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 @@ -41,69 +54,74 @@ testNotificationsForOfflineBackends = do downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) - -- Even when a participating backend is down, messages to conversations - -- owned by other backends should go. - successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" - successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" - let successfulMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess - - -- When conversation owning backend is down, messages will fail to be sent. - failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" - failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" - let failedMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> - -- Due to the way federation breaks in local env vs K8s, it can return 521 - -- (local) or 533 (K8s). - resp.status `shouldMatchOneOf` [Number 521, Number 533] - - -- Conversation creation with people from down backend should fail - bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Adding users to an up backend conversation should not work when one of - -- the participating backends is down. This is due to not being able to - -- check non-fully connected graph between all participating backends - -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check - -- and the user can be added as long as the backend is reachable - otherUser3 <- randomUser OtherDomain def - connectUsers delUser otherUser3 - bindResponse (addMembers delUser upBackendConv [otherUser3]) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- Adding users from down backend to a conversation should also fail - bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Removing users from an up backend conversation should work even when one - -- of the participating backends is down. - bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- User deletions should eventually make it to the other backend. - deleteUser delUser - - let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] - isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] - - do - newMsgNotif <- awaitNotification otherUser otherClient noValue 1 isNewMessageNotif - newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv - newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" - - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveUpConvNotif - - delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif - objQid delUserDeletedNotif `shouldMatch` objQid delUser + withWebSocket otherUser $ \ws -> do + -- Even when a participating backend is down, messages to conversations + -- owned by other backends should go. + successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" + successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" + let successfulMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess + + -- When the conversation owning backend is down, messages will fail to be sent. + failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" + failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" + let failedMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> + -- Due to the way federation breaks in local env vs K8s, it can return 521 + -- (local) or 533 (K8s). + resp.status `shouldMatchOneOf` [Number 521, Number 533] + + -- Conversation creation with people from down backend should fail + bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Adding users to an up backend conversation should not work when one of + -- the participating backends is down. This is due to not being able to + -- check non-fully connected graph between all participating backends + -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check + -- and the user can be added as long as the backend is reachable + otherUser3 <- randomUser OtherDomain def + connectUsers delUser otherUser3 + bindResponse (addMembers delUser upBackendConv def {users = [otherUser3]}) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Adding users from down backend to a conversation should fail + bindResponse (addMembers delUser upBackendConv def {users = [downUser2]}) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Removing users from an up backend conversation should work even when one + -- of the participating backends is down. + bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Even removing a user from the down backend itself should work. + bindResponse (removeMember delUser upBackendConv delUser) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- User deletions should eventually make it to the other backend. + deleteUser delUser + + let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] + isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] + + do + newMsgNotif <- awaitMatch 10 isNewMessageNotif ws + newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" + + void $ awaitMatch 10 isOtherUser2LeaveUpConvNotif ws + void $ awaitMatch 10 isDelUserLeaveUpConvNotif ws + + delUserDeletedNotif <- nPayload $ awaitMatch 10 isDeleteUserNotif ws + objQid delUserDeletedNotif `shouldMatch` objQid delUser runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do newMsgNotif <- awaitNotification downUser1 downClient1 noValue 5 isNewMessageNotif @@ -124,8 +142,3 @@ testNotificationsForOfflineBackends = do delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser - -allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool -allPreds [] _ = pure True -allPreds [p] x = p x -allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/integration/test/Test/MessageTimer.hs b/integration/test/Test/MessageTimer.hs new file mode 100644 index 0000000000..6a401e3d68 --- /dev/null +++ b/integration/test/Test/MessageTimer.hs @@ -0,0 +1,55 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.MessageTimer where + +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testMessageTimerChangeWithRemotes :: HasCallStack => App () +testMessageTimerChangeWithRemotes = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testMessageTimerChangeWithUnreachableRemotes :: HasCallStack => App () +testMessageTimerChangeWithUnreachableRemotes = do + resourcePool <- asks resourcePool + alice <- randomUser OwnDomain def + conv <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + bob <- randomUser dynBackend.berDomain def + connectUsers alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice diff --git a/integration/test/Test/Roles.hs b/integration/test/Test/Roles.hs new file mode 100644 index 0000000000..906d9d9632 --- /dev/null +++ b/integration/test/Test/Roles.hs @@ -0,0 +1,65 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Roles where + +import API.Galley +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude + +testRoleUpdateWithRemotesOk :: HasCallStack => App () +testRoleUpdateWithRemotesOk = do + [bob, charlie, alice] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie, alice] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + bindResponse (getConversation bob conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject charlie + resp.json %. "members.others.0.conversation_role" `shouldMatch` "wire_admin" + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob + +testRoleUpdateWithRemotesUnreachable :: HasCallStack => App () +testRoleUpdateWithRemotesUnreachable = do + [bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain] + startDynamicBackends [mempty] $ \[dynBackend] -> do + alice <- randomUser dynBackend def + mapM_ (connectUsers alice) [bob, charlie] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 299c1ae20e..8be47a687b 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -4,6 +4,7 @@ module Testlib.Assertions where import Control.Exception as E import Control.Monad.Reader +import Data.Aeson (Value) import Data.Aeson qualified as Aeson import Data.Aeson.Encode.Pretty qualified as Aeson import Data.ByteString.Base64 qualified as B64 @@ -127,6 +128,9 @@ shouldMatchSet a b = do lb <- fmap sort (asList b) la `shouldMatch` lb +shouldBeEmpty :: (MakesValue a, HasCallStack) => a -> App () +shouldBeEmpty a = a `shouldMatch` (mempty :: [Value]) + shouldMatchOneOf :: (MakesValue a, MakesValue b, HasCallStack) => a -> diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 05a04f366a..27c9db153c 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -66,6 +66,9 @@ module Testlib.Prelude -- * Functor (<$$>), (<$$$>), + + -- * Applicative + allPreds, ) where @@ -222,3 +225,11 @@ infix 4 <$$> (<$$$>) = fmap . fmap . fmap infix 4 <$$$> + +---------------------------------------------------------------------- +-- Applicative + +allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool +allPreds [] _ = pure True +allPreds [p] x = p x +allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 3560e2c5e4..3fa1aba287 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -77,7 +77,7 @@ sendNotification env component path body = runFederatorClient env . void $ clientIn (Proxy @BackendNotificationAPI) (Proxy @(FederatorClient c)) (withoutFirstSlash path) body -enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c () -> IO () +enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c a -> IO a enqueue channel originDomain targetDomain deliveryMode (FedQueueClient action) = runReaderT action FedQueueEnv {..} diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 8d8b9883cc..3015606171 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -69,7 +69,11 @@ data SomeConversationAction where instance Show SomeConversationAction where show (SomeConversationAction tag action) = - $(sCases ''ConversationActionTag [|tag|] [|show action|]) + "SomeConversationAction {tag = " + <> show (fromSing tag) + <> ", action = " + <> $(sCases ''ConversationActionTag [|tag|] [|show action|]) + <> "}" instance Eq SomeConversationAction where (SomeConversationAction tag1 action1) == (SomeConversationAction tag2 action2) = diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index e824b4f53e..66792c8303 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -154,7 +154,9 @@ notifyUserDeleted self remotes = do remoteDomain = tDomain remotes view rabbitmqChannel >>= \case Just chanVar -> do - enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ void $ fedQueueClient @'Brig @"on-user-deleted-connections" notif + enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ + void $ + fedQueueClient @'Brig @"on-user-deleted-connections" notif Nothing -> Log.err $ Log.msg ("Federation error while notifying remote backends of a user deletion." :: ByteString) @@ -163,7 +165,7 @@ notifyUserDeleted self remotes = do . Log.field "error" (show FederationNotConfigured) -- | Enqueues notifications in RabbitMQ. Retries 3 times with a delay of 1s. -enqueueNotification :: (MonadReader Env m, MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () +enqueueNotification :: (MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do let policy = limitRetries 3 <> constantDelay 1_000_000 recovering policy [logRetries (const $ pure True) logError] (const go) diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index 25d95fd6ba..cad9c16082 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -31,7 +31,6 @@ import Data.Domain import Data.Handle import Data.Id import Data.Json.Util (toBase64Text) -import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List1 as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens @@ -49,7 +48,6 @@ import Util import Util.Options (Endpoint) import Wire.API.Asset import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation @@ -99,7 +97,6 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "claim multi-prekey bundle" $ testClaimMultiPrekeyBundleSuccess brig brigTwo, test mg "list user clients" $ testListUserClients brig brigTwo, test mg "list own conversations" $ testListConversations brig brigTwo galley galleyTwo, - test mg "add remote users to local conversation" $ testAddRemoteUsersToLocalConv brig galley brigTwo galleyTwo, test mg "remove remote user from a local conversation" $ testRemoveRemoteUserFromLocalConv brig galley brigTwo galleyTwo, test mg "leave a remote conversation" $ leaveRemoteConversation brig galley brigTwo galleyTwo, test mg "include remote users to new conversation" $ testRemoteUsersInNewConv brig galley brigTwo galleyTwo, @@ -249,63 +246,6 @@ testClaimMultiPrekeyBundleSuccess brig1 brig2 = do const 200 === statusCode const (Just ucm) === responseJsonMaybe -testAddRemoteUsersToLocalConv :: Brig -> Galley -> Brig -> Galley -> Http () -testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do - alice <- randomUser brig1 - bob <- randomUser brig2 - - let newConv = - NewConv - [] - [] - (checked "gossip") - mempty - Nothing - Nothing - Nothing - Nothing - roleNameWireAdmin - ProtocolProteusTag - convId <- - fmap cnvQualifiedId . responseJsonError - =<< post - ( galley1 - . path "/conversations" - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json newConv - ) - - connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) - - let invite = InviteQualified (userQualifiedId bob :| []) roleNameWireAdmin - post - ( apiVersion "v1" - . galley1 - . paths ["conversations", (toByteString' . qUnqualified) convId, "members", "v2"] - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json invite - ) - !!! (const 200 === statusCode) - - -- test GET /conversations/:domain/:cnv -- Alice's domain is used here - liftIO $ putStrLn "search for conversation on backend 1..." - res <- getConvQualified galley1 (userId alice) convId Galley -> Brig -> Galley -> Http () testRemoveRemoteUserFromLocalConv brig1 galley1 brig2 galley2 = do alice <- randomUser brig1 diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 710309b809..2efa6ca4d7 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -71,6 +71,7 @@ import Galley.Data.Conversation qualified as Data import Galley.Data.Scope (Scope (ReusableCode)) import Galley.Data.Services import Galley.Effects +import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.BotAccess qualified as E import Galley.Effects.BrigAccess qualified as E import Galley.Effects.CodeStore qualified as E @@ -88,6 +89,7 @@ import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation import Imports hiding ((\\)) +import Network.AMQP qualified as Q import Polysemy import Polysemy.Error import Polysemy.Input @@ -349,6 +351,7 @@ ensureAllowed tag loc action conv origUser = do performAction :: forall tag r. ( HasConversationActionEffects tag r, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r ) => Sing tag -> @@ -419,7 +422,8 @@ performAction tag origUser lconv action = do performConversationJoin :: forall r. - ( HasConversationActionEffects 'ConversationJoinTag r + ( HasConversationActionEffects 'ConversationJoinTag r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -529,7 +533,8 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, - Member (Error FederationError) r + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -615,13 +620,13 @@ data LocalConversationUpdate = LocalConversationUpdate updateLocalConversation :: forall tag r. - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -656,12 +661,12 @@ updateLocalConversation lcnv qusr con action = do updateLocalConversationUnchecked :: forall tag r. ( SingI tag, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -704,6 +709,7 @@ updateLocalConversationUserUnchecked :: forall tag r. ( SingI tag, HasConversationActionEffects tag r, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r ) => Local Conversation -> @@ -762,7 +768,7 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input UTCTime) r, @@ -790,24 +796,23 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do (SomeConversationAction tag action) update <- do + let remoteTargets = toList (bmRemotes targets) updates <- - E.runFederatedConcurrentlyEither (toList (bmRemotes targets)) $ - \ruids -> do - let update = mkUpdate (tUnqualified ruids) - -- if notifyOrigDomain is false, filter out user from quid's domain, - -- because quid's backend will update local state and notify its users - -- itself using the ConversationUpdate returned by this function - if notifyOrigDomain || tDomain ruids /= qDomain quid - then fedClient @'Galley @"on-conversation-updated" update $> Nothing - else pure (Just update) - let f = fromMaybe (mkUpdate []) . asum . map tUnqualified . rights - update = f updates - failedUpdates = lefts updates - for_ failedUpdates $ - logError - "on-conversation-updated" - "An error occurred while communicating with federated server: " - pure update + enqueueNotificationsConcurrently Q.Persistent remoteTargets $ \ruids -> do + let update = mkUpdate (tUnqualified ruids) + -- if notifyOrigDomain is false, filter out user from quid's domain, + -- because quid's backend will update local state and notify its users + -- itself using the ConversationUpdate returned by this function + if notifyOrigDomain || tDomain ruids /= qDomain quid + then fedQueueClient @'Galley @"on-conversation-updated" update $> Nothing + else pure (Just update) + case partitionEithers updates of + (ls :: [Remote ([UserId], FederationError)], rs) -> do + for_ ls $ + logError + "on-conversation-updated" + "An error occurred while communicating with federated server: " + pure $ fromMaybe (mkUpdate []) . asum . map tUnqualified $ rs -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -816,10 +821,12 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- to the originating domain (if it is remote) pure $ LocalConversationUpdate e update where - logError :: (Show a) => String -> String -> (a, FederationError) -> Sem r () + logError :: String -> String -> Remote (a, FederationError) -> Sem r () logError field msg e = P.warn $ - Log.field "federation call" field . Log.msg (msg <> show e) + Log.field "federation call" field + . Log.field "domain" (_domainText (tDomain e)) + . Log.msg (msg <> displayException (snd (tUnqualified e))) -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. @@ -938,7 +945,8 @@ addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do -- leave, but then sends notifications as if the user was removed by someone -- else. kickMember :: - ( Member (Error FederationError) r, + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 2642e428a7..18bb120620 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -51,7 +51,6 @@ import Galley.API.Util import Galley.App import Galley.Data.Conversation qualified as Data import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FireAndForget qualified as E import Galley.Effects.MemberStore qualified as E @@ -219,7 +218,8 @@ onConversationUpdated requestingDomain cu = do -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, @@ -365,7 +365,8 @@ sendMessage originDomain msr = do throwErr = throw . InvalidPayload . LT.pack onUserDeleted :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member FederatorAccess r, Member FireAndForget r, Member ExternalAccess r, @@ -421,7 +422,8 @@ onUserDeleted origDomain udcn = do updateConversation :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member BotAccess r, Member FireAndForget r, @@ -535,7 +537,8 @@ handleMLSMessageErrors = . mapToGalleyError @MLSBundleStaticErrors sendMLSCommitBundle :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, @@ -568,7 +571,8 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle sendMLSMessage :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index b3b3d6b34b..91a9ff45df 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -139,7 +139,8 @@ getSettings lzusr tid = do removeSettingsInternalPaging :: forall r. - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -181,7 +182,8 @@ removeSettings :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -243,7 +245,8 @@ removeSettings' :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), - ( Member BotAccess r, + ( Member BackendNotificationQueueAccess r, + Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, @@ -335,7 +338,8 @@ getUserStatus _lzusr tid uid = do -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). grantConsent :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -372,7 +376,8 @@ grantConsent lusr tid = do -- | Request to provision a device on the legal hold service for a user requestDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -450,7 +455,8 @@ requestDevice lzusr tid uid = do -- since they are replaced if needed when registering new LH devices. approveDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -528,7 +534,8 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw disableForUser :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -585,7 +592,8 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = -- or disabled, make sure the affected connections are screened for policy conflict (anybody -- with no-consent), and put those connections in the appropriate blocked state. changeLegalholdStatus :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -702,7 +710,8 @@ unsetTeamLegalholdWhitelistedH tid = do -- contains the hypothetical new LH status of `uid`'s so it can be consulted instead of the -- one from the database. handleGroupConvPolicyConflicts :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 643048311c..781e8258f2 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -547,7 +547,8 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do MLSMessageResponseNonFederatingBackends e -> throw e type HasProposalEffects r = - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (Error MLSProposalFailure) r, @@ -1094,7 +1095,8 @@ checkExternalProposalUser qusr prop = do executeProposalAction :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 921990a56f..c9d800f59a 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1082,7 +1082,8 @@ getTeamConversation zusr tid cid = do >>= noteS @'ConvNotFound deleteTeamConversation :: - ( Member CodeStore r, + ( Member BackendNotificationQueueAccess r, + Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, @@ -1090,7 +1091,6 @@ deleteTeamConversation :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'DeleteConversation)) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member TeamStore r, diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 8b87f4850c..4dd3dc85b1 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -284,6 +284,7 @@ instance SetFeatureConfig LegalholdConfig where type SetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), + Member BackendNotificationQueueAccess r, Member BotAccess r, Member BrigAccess r, Member CodeStore r, diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index eb3253d10a..3c71250598 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -94,7 +94,6 @@ import Galley.Data.Conversation qualified as Data import Galley.Data.Services as Data import Galley.Data.Types hiding (Conversation) import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E import Galley.Effects.ConversationStore qualified as E @@ -253,7 +252,8 @@ handleUpdateResult = \case Unchanged -> empty & setStatus status204 type UpdateConversationAccessEffects = - '[ BotAccess, + '[ BackendNotificationQueueAccess, + BotAccess, BrigAccess, CodeStore, ConversationStore, @@ -307,7 +307,8 @@ updateConversationAccessUnqualified lusr con cnv update = update updateConversationReceiptMode :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -380,7 +381,8 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do updateLocalStateOfRemoteConv (qualifyAs rcnv convUpdate) (Just conn) >>= note NoChanges updateConversationReceiptModeUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -403,13 +405,13 @@ updateConversationReceiptModeUnqualified :: updateConversationReceiptModeUnqualified lusr zcon cnv = updateConversationReceiptMode lusr zcon (tUntagged (qualifyAs lusr cnv)) updateConversationMessageTimer :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -436,13 +438,13 @@ updateConversationMessageTimer lusr zcon qcnv update = qcnv updateConversationMessageTimerUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -455,7 +457,8 @@ updateConversationMessageTimerUnqualified :: updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMessageTimer lusr zcon (tUntagged (qualifyAs lusr cnv)) deleteLocalConversation :: - ( Member CodeStore r, + ( Member BackendNotificationQueueAccess r, + Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'NotATeamMember) r, @@ -463,7 +466,6 @@ deleteLocalConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member TeamStore r, @@ -672,7 +674,8 @@ checkReusableCode convCode = do joinConversationByReusableCode :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member ConversationStore r, Member (ErrorS 'CodeNotFound) r, @@ -683,7 +686,6 @@ joinConversationByReusableCode :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, - Member FederatorAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input Opts) r, @@ -705,8 +707,8 @@ joinConversationByReusableCode lusr zcon req = do joinConversationById :: forall r. - ( Member BrigAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, @@ -731,8 +733,8 @@ joinConversationById lusr zcon cnv = do joinConversation :: forall r. - ( Member BrigAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, @@ -774,7 +776,8 @@ joinConversation lusr zcon conv access = do action addMembers :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, @@ -813,7 +816,8 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualifiedV2 :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -852,7 +856,8 @@ addMembersUnqualifiedV2 lusr zcon cnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -953,7 +958,8 @@ updateUnqualifiedSelfMember lusr zcon cnv update = do updateSelfMember lusr zcon (tUntagged lcnv) update updateOtherMemberLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -961,7 +967,6 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -980,7 +985,8 @@ updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult ConversationMemberUpdate qvictim update updateOtherMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -988,7 +994,6 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1006,7 +1011,8 @@ updateOtherMemberUnqualified lusr zcon cnv victim update = do updateOtherMemberLocalConv lcnv lusr zcon (tUntagged lvictim) update updateOtherMember :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1014,7 +1020,6 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1041,7 +1046,8 @@ updateOtherMemberRemoteConv :: updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented removeMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1067,7 +1073,8 @@ removeMemberUnqualified lusr con cnv victim = do removeMemberQualified lusr con (tUntagged lcnv) (tUntagged lvictim) removeMemberQualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1134,7 +1141,8 @@ removeMemberFromRemoteConv cnv lusr victim -- | Remove a member from a local conversation. removeMemberFromLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'LeaveConversation)) r, @@ -1326,14 +1334,14 @@ postOtrMessageUnqualified sender zcon cnv = (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv) updateConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1352,14 +1360,14 @@ updateConversationName lusr zcon qcnv convRename = do convRename updateUnqualifiedConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1374,14 +1382,14 @@ updateUnqualifiedConversationName lusr zcon cnv rename = do updateLocalConversationName lusr zcon lcnv rename updateLocalConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index a8dc2a5198..535a60eade 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -56,6 +56,9 @@ module Galley.Effects -- * Polysemy re-exports Member, Members, + + -- * Queueing effects + BackendNotificationQueueAccess, ) where diff --git a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs index c7ecdfcb77..ac006ded4c 100644 --- a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs +++ b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs @@ -15,7 +15,13 @@ data BackendNotificationQueueAccess m a where KnownComponent c => Remote x -> Q.DeliveryMode -> - FedQueueClient c () -> - BackendNotificationQueueAccess m (Either FederationError ()) + FedQueueClient c a -> + BackendNotificationQueueAccess m (Either FederationError a) + EnqueueNotificationsConcurrently :: + (KnownComponent c, Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + BackendNotificationQueueAccess m [Either (Remote ([x], FederationError)) (Remote a)] makeSem ''BackendNotificationQueueAccess diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index fb2e02605f..b9affe4858 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -5,6 +5,7 @@ module Galley.Intra.BackendNotificationQueue (interpretBackendNotificationQueueA import Control.Lens (view) import Control.Monad.Catch import Control.Retry +import Data.Bifunctor import Data.Domain import Data.Qualified import Galley.Effects.BackendNotificationQueueAccess (BackendNotificationQueueAccess (..)) @@ -16,7 +17,7 @@ import Network.AMQP qualified as Q import Polysemy import Polysemy.Input import System.Logger.Class qualified as Log -import UnliftIO.Timeout (timeout) +import UnliftIO import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Error @@ -29,8 +30,10 @@ interpretBackendNotificationQueueAccess :: interpretBackendNotificationQueueAccess = interpret $ \case EnqueueNotification remote deliveryMode action -> do embedApp $ enqueueNotification (tDomain remote) deliveryMode action + EnqueueNotificationsConcurrently m xs rpc -> do + embedApp $ enqueueNotificationsConcurrently m xs rpc -enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c () -> App (Either FederationError ()) +enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> App (Either FederationError a) enqueueNotification remoteDomain deliveryMode action = do mChanVar <- view rabbitmqChannel ownDomain <- view (options . settings . federationDomain) @@ -56,6 +59,19 @@ enqueueNotification remoteDomain deliveryMode action = do Just chan -> do liftIO $ enqueue chan ownDomain remoteDomain deliveryMode action +enqueueNotificationsConcurrently :: + (Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + App [(Either (Remote ([x], FederationError)) (Remote a))] +enqueueNotificationsConcurrently m xs f = + pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + bimap + (qualifyAs r . (tUnqualified r,)) + (qualifyAs r) + <$> enqueueNotification (tDomain r) m (f r) + data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 179b3fbb4a..014ffa6382 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -51,7 +51,6 @@ import Data.ByteString qualified as BS import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Domain -import Data.Either.Extra (eitherToMaybe) import Data.Id import Data.Json.Util (toBase64Text, toUTCTimeMillis) import Data.List.NonEmpty (NonEmpty (..)) @@ -106,7 +105,6 @@ import Wire.API.Routes.Version import Wire.API.Routes.Versioned import Wire.API.Team.Feature qualified as Public import Wire.API.Team.Member qualified as Teams -import Wire.API.User import Wire.API.User.Client import Wire.API.UserMap (UserMap (..)) @@ -177,9 +175,6 @@ tests s = test s "generate guest link forbidden when no guest or non-team-member access role" generateGuestLinkFailIfNoNonTeamMemberOrNoGuestAccess, test s "fail to add members when not connected" postMembersFail, test s "fail to add too many members" postTooManyMembersFail, - test s "add remote members" testAddRemoteMember, - test s "delete conversation with remote members" testDeleteTeamConversationWithRemoteMembers, - test s "delete conversation with unavailable remote members" testDeleteTeamConversationWithUnavailableRemoteMembers, test s "get conversations/:domain/:cnv - local" testGetQualifiedLocalConv, test s "get conversations/:domain/:cnv - local, not found" testGetQualifiedLocalConvNotFound, test s "get conversations/:domain/:cnv - local, not participating" testGetQualifiedLocalConvNotParticipating, @@ -193,9 +188,6 @@ tests s = test s "delete conversations/:domain/:cnv/members/:domain/:usr - fail, self conv" deleteMembersQualifiedFailSelf, test s "delete conversations/:domain:/cnv/members/:domain/:usr - fail, 1:1 conv" deleteMembersQualifiedFailO2O, test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with all locals" deleteMembersConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete local" deleteLocalMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete remote" deleteRemoteMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete unavailable remote" deleteUnavailableRemoteMemberConvLocalQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv" leaveRemoteConvQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, non-existent" leaveNonExistentRemoteConv, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, denied" leaveRemoteConvDenied, @@ -204,8 +196,6 @@ tests s = test s "rename conversation (deprecated endpoint)" putConvDeprecatedRenameOk, test s "rename conversation" putConvRenameOk, test s "rename qualified conversation" putQualifiedConvRenameOk, - test s "rename qualified conversation with remote members" putQualifiedConvRenameWithRemotesOk, - test s "rename qualified conversation with unavailable remote" putQualifiedConvRenameWithRemotesUnavailable, test s "rename qualified conversation failure" putQualifiedConvRenameFailure, test s "other member update role" putOtherMemberOk, test s "qualified other member update role" putQualifiedOtherMemberOk, @@ -218,8 +208,6 @@ tests s = test s "remote conversation member update (otr hidden)" putRemoteConvMemberHiddenOk, test s "remote conversation member update (everything)" putRemoteConvMemberAllOk, test s "conversation receipt mode update" putReceiptModeOk, - test s "conversation receipt mode update with remote members" putReceiptModeWithRemotesOk, - test s "conversation receipt mode update with unavailable remote members" putReceiptModeWithRemotesUnavailable, test s "remote conversation receipt mode update" putRemoteReceiptModeOk, test s "leave connect conversation" leaveConnectConversation, test s "post conversations/:cnv/otr/message: message delivery and missing clients" postCryptoMessageVerifyMsgSentAndRejectIfMissingClient, @@ -240,8 +228,6 @@ tests s = test s "join code-access conversation - password" postJoinCodeConvWithPassword, test s "convert invite to code-access conversation" postConvertCodeConv, test s "convert code to team-access conversation" postConvertTeamConv, - test s "local and remote guests are removed when access changes" testAccessUpdateGuestRemoved, - test s "local and remote guests are removed when access changes remotes unavailable" testAccessUpdateGuestRemovedRemotesUnavailable, test s "team member can't join via guest link if access role removed" testTeamMemberCantJoinViaGuestLinkIfAccessRoleRemoved, test s "cannot join private conversation" postJoinConvFail, test s "revoke guest links for team conversation" testJoinTeamConvGuestLinksDisabled, @@ -1628,183 +1614,6 @@ postConvertTeamConv = do -- team members (dave) can still join postJoinCodeConv dave j !!! const 200 === statusCode --- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 --- --- The test asserts that, among others, remote users are removed from a --- conversation when an access update occurs that disallows guests from --- accessing. -testAccessUpdateGuestRemoved :: TestM () -testAccessUpdateGuestRemoved = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - -- dee is a remote guest - let remoteDomain = Domain "far-away.example.com" - dee <- Qualified <$> randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply EmptyResponse) $ do - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - -- dee's remote receives a notification - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ do - -- This request should still succeed even with an unresponsive federation member. - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - viewFederationDomain deleteMemberQualified alice qalice qc !!! const 403 === statusCode -testAddRemoteMember :: TestM () -testAddRemoteMember = do - qalice <- randomQualifiedUser - let alice = qUnqualified qalice - let localDomain = qDomain qalice - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - postQualifiedMembers alice (remoteBob :| []) qconvId !!! do - const 403 === statusCode - const (Right (Just "not-connected")) === fmap (view (at "label")) . responseJsonEither @Object - - connectWithRemoteUser alice remoteBob - - (resp, reqs) <- - withTempMockFederator' (respond remoteBob) $ - postQualifiedMembers alice (remoteBob :| []) qconvId - getConvQualified alice qconvId - liftIO $ do - let actual = cmOthers $ cnvMembers conv - let expected = [OtherMember remoteBob Nothing roleNameWireAdmin] - assertEqual "other members should include remoteBob" expected actual - where - respond :: Qualified UserId -> Mock LByteString - respond bob = - asum - [ getNotFullyConnectedBackendsMock <|> guardComponent Brig *> mockReply [mkProfile bob (Name "bob")] - ] - -testDeleteTeamConversationWithRemoteMembers :: TestM () -testDeleteTeamConversationWithRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = getNotFullyConnectedBackendsMock <|> "api-version" ~> EmptyResponse - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - -testDeleteTeamConversationWithUnavailableRemoteMembers :: TestM () -testDeleteTeamConversationWithUnavailableRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = - getNotFullyConnectedBackendsMock - <|> - -- Mock an unavailable federation server for the deletion call - (guardRPC "on-conversation-updated" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - <|> (guardRPC "delete-team-conversation" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - testGetQualifiedLocalConv :: TestM () testGetQualifiedLocalConv = do alice <- randomUser @@ -3122,180 +2819,6 @@ deleteMembersConvLocalQualifiedOk = do deleteMemberQualified alice qAlice qconv !!! const 200 === statusCode deleteMemberQualified alice qAlice qconv !!! const 404 === statusCode --- Creates a conversation with three users. Alice and Bob are on the local --- domain, while Eve is on a remote domain. It uses a qualified endpoint for --- removing Bob from the conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteLocalMemberConvLocalQualifiedOk :: TestM () -deleteLocalMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - eve <- randomId - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain = Domain "far-away.example.com" - qEve = Qualified eve remoteDomain - - connectUsers alice (singleton bob) - connectWithRemoteUser alice qEve - convId <- - decodeConvId - <$> postConvWithRemoteUsers - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qEve]} - let qconvId = Qualified convId localDomain - - let mockReturnEve = - mockedFederatedBrigResponse [(qEve, "Eve")] - <|> mockReply EmptyResponse - (respDel, fedRequests) <- - withTempMockFederator' mockReturnEve $ - deleteMemberQualified alice qBob qconvId - let [galleyFederatedRequest] = fedRequestsForDomain remoteDomain Galley fedRequests - assertRemoveUpdate galleyFederatedRequest qconvId qAlice [qUnqualified qEve] qBob - - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qBob] e - - -- Now that Bob is gone, try removing him once again - deleteMemberQualified alice qBob qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteRemoteMemberConvLocalQualifiedOk :: TestM () -deleteRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedResponse = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - (convId, _) <- - withTempMockFederator' (getNotFullyConnectedBackendsMock <|> mockedResponse <|> mockReply EmptyResponse) $ - fmap decodeConvId $ - postConvQualified - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedResponse <|> mockReply EmptyResponse) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation. The federator for far-away-2.example.com isn't availabe: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteUnavailableRemoteMemberConvLocalQualifiedOk :: TestM () -deleteUnavailableRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedGetUsers remote2Response = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> remote2Response - ] - mockedCreateConvGetUsers = - mockedGetUsers (mockReply [mkProfile qEve (Name "Eve")]) - mockedRemMemGetUsers = - mockedGetUsers (throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - mockedOther = do - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply EmptyResponse, - guard (d == remoteDomain2) - *> asum - [ guardRPC "on-conversation-created" *> mockReply EmptyResponse, - guardRPC "on-conversation-updated" *> mockReply EmptyResponse, - throw $ MockErrorResponse HTTP.status503 "Down for maintenance." - ] - ] - convId <- - fmap decodeConvId $ - postConvWithRemoteUsersGeneric - (mockedCreateConvGetUsers <|> mockedOther) - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedOther) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - -- Alice, a local user, leaves a remote conversation. Bob's domain is the same -- as that of the conversation. The test uses the following endpoint: -- @@ -3461,86 +2984,6 @@ putQualifiedConvRenameOk = do evtFrom e @?= qbob evtData e @?= EdConvRename (ConversationRename "gossip++") -putQualifiedConvRenameWithRemotesOk :: TestM () -putQualifiedConvRenameWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - -putQualifiedConvRenameWithRemotesUnavailable :: TestM () -putQualifiedConvRenameWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - putConvDeprecatedRenameOk :: TestM () putConvDeprecatedRenameOk = do c <- view tsCannon @@ -3987,90 +3430,6 @@ putRemoteReceiptModeOk = do WS.assertMatch_ (5 # Second) wsAdam $ \n -> do liftIO $ wsAssertConvReceiptModeUpdate qconv qalice newReceiptMode n -putReceiptModeWithRemotesOk :: TestM () -putReceiptModeWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - -putReceiptModeWithRemotesUnavailable :: TestM () -putReceiptModeWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - postTypingIndicatorsV2 :: TestM () postTypingIndicatorsV2 = do c <- view tsCannon diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 73930b4bd5..d0b470937c 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -23,7 +23,6 @@ import Bilge hiding (head) import Bilge.Assert import Control.Exception import Control.Lens hiding ((#)) -import Data.Aeson qualified as A import Data.ByteString.Conversion (toByteString') import Data.Domain import Data.Id @@ -34,7 +33,6 @@ import Data.List1 qualified as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens import Data.Qualified -import Data.Range import Data.Set qualified as Set import Data.Singletons import Data.Time.Clock @@ -83,13 +81,10 @@ tests s = test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update" notifyReceiptMode, test s "POST /federation/on-conversation-updated : Notify local user about access update" notifyAccess, test s "POST /federation/on-conversation-updated : Notify local users about a deleted conversation" notifyDeletedConversation, - test s "POST /federation/leave-conversation : Success" leaveConversationSuccess, test s "POST /federation/leave-conversation : Non-existent" leaveConversationNonExistent, test s "POST /federation/leave-conversation : Invalid type" leaveConversationInvalidType, test s "POST /federation/on-message-sent : Receive a message from another backend" onMessageSent, test s "POST /federation/send-message : Post a message sent from another backend" sendMessage, - test s "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" onUserDeleted, - test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin, test s "POST /federation/on-conversation-updated : Notify local user about conversation rename with an unavailable federator" notifyConvRenameUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about message timer update with an unavailable federator" notifyMessageTimerUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update with an unavailable federator" notifyReceiptModeUnavailable, @@ -711,69 +706,6 @@ addRemoteUser = do WS.assertNoEvent (1 # Second) [wsC] WS.assertNoEvent (1 # Second) [wsF] -leaveConversationSuccess :: TestM () -leaveConversationSuccess = do - localDomain <- viewFederationDomain - c <- view tsCannon - [alice, bob] <- randomUsers 2 - let qBob = Qualified bob localDomain - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - connectWithRemoteUser alice qChad - connectWithRemoteUser alice qDee - connectWithRemoteUser alice qEve - - let mock = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - - convId <- - decodeConvId - <$> postConvWithRemoteUsersGeneric - (mock <|> mockReply EmptyResponse) - alice - Nothing - defNewProteusConv - { newConvQualifiedUsers = [qBob, qChad, qDee, qEve] - } - let qconvId = Qualified convId localDomain - - (_, federatedRequests) <- - WS.bracketR2 c alice bob $ \(wsAlice, wsBob) -> do - withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty <|> mock <|> mockReply EmptyResponse) $ do - g <- viewGalley - let leaveRequest = FedGalley.LeaveConversationRequest convId (qUnqualified qChad) - respBS <- - post - ( g - . paths ["federation", "leave-conversation"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' remoteDomain1) - . json leaveRequest - ) - Causes Alice to be notified --- - groupConvId -> Causes Alice and Alex to be notified --- - extraConvId -> Ignored --- - noBobConvId -> Ignored -onUserDeleted :: TestM () -onUserDeleted = do - cannon <- view tsCannon - let bDomain = Domain "b.far-away.example.com" - cDomain = Domain "c.far-away.example.com" - - alice <- qTagUnsafe <$> randomQualifiedUser - alex <- randomQualifiedUser - (bob, ooConvId) <- generateRemoteAndConvIdWithDomain bDomain True alice - bart <- randomQualifiedId bDomain - carl <- randomQualifiedId cDomain - - connectWithRemoteUser (tUnqualified alice) (tUntagged bob) - connectUsers (tUnqualified alice) (pure (qUnqualified alex)) - connectWithRemoteUser (tUnqualified alice) bart - connectWithRemoteUser (tUnqualified alice) carl - - -- create 1-1 conversation between alice and bob - createOne2OneConvWithRemote alice bob - - -- create group conversation with everybody - groupConvId <- WS.bracketR cannon (tUnqualified alice) $ \wsAlice -> do - convId <- - decodeQualifiedConvId - <$> ( postConvWithRemoteUsers - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [tUntagged bob, alex, bart, carl]} - do - convId <- - fmap decodeQualifiedConvId $ - postConvQualified - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [alex]} - do - (resp, rpcCalls) <- withTempMockFederator' (mockReply EmptyResponse) $ do - let udcn = - FedGalley.UserDeletedConversationsNotification - { FedGalley.user = tUnqualified bob, - FedGalley.conversations = - unsafeRange - [ qUnqualified ooConvId, - qUnqualified groupConvId, - extraConvId, - qUnqualified noBobConvId - ] - } - g <- viewGalley - responseJsonError - =<< post - ( g - . paths ["federation", "on-user-deleted-conversations"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' (tDomain bob)) - . json udcn - ) - show rpcCalls) 1 (length rpcCalls) - - -- Assertions about RPC to 'cDomain' - cDomainRPC <- assertOne $ filter (\c -> frTargetDomain c == cDomain) rpcCalls - cDomainRPCReq <- assertRight $ parseFedRequest cDomainRPC - FedGalley.cuOrigUserId cDomainRPCReq @?= tUntagged bob - FedGalley.cuConvId cDomainRPCReq @?= qUnqualified groupConvId - FedGalley.cuAlreadyPresentUsers cDomainRPCReq @?= [qUnqualified carl] - FedGalley.cuAction cDomainRPCReq @?= SomeConversationAction (sing @'ConversationLeaveTag) () - --- | We test only ReceiptMode update here --- --- A : local domain, owns the conversation --- B : bob is an admin of the converation --- C : charlie is a regular member of the conversation -updateConversationByRemoteAdmin :: TestM () -updateConversationByRemoteAdmin = do - c <- view tsCannon - (alice, qalice) <- randomUserTuple - - let bdomain = Domain "b.example.com" - cdomain = Domain "c.example.com" - qbob <- randomQualifiedId bdomain - qcharlie <- randomQualifiedId cdomain - mapM_ (connectWithRemoteUser alice) [qbob, qcharlie] - - let convName = "Test Conv" - WS.bracketR c alice $ \wsAlice -> do - (rsp, _federatedRequests) <- do - let mock = ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) <|> mockReply EmptyResponse - withTempMockFederator' mock $ do - postConvQualified alice Nothing defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} - assertFailure ("Expected ConversationUpdateResponseUpdate but got " <> show err) - ConversationUpdateResponseNoChanges -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNoChanges" - ConversationUpdateResponseUpdate up -> pure up - ConversationUpdateResponseNonFederatingBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNonFederatingBackends" - ConversationUpdateResponseUnreachableBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseUnreachableBackends" - - liftIO $ do - cuOrigUserId cnvUpdate' @?= qbob - cuAlreadyPresentUsers cnvUpdate' @?= [qUnqualified qbob] - cuAction cnvUpdate' @?= action - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertConvReceiptModeUpdate cnv qalice newReceiptMode n - - -- backend B does *not* get notified of the conversation update ony of bob's promotion - liftIO $ do - [(_fr, cUpdate)] <- mapM parseConvUpdate $ filter (\r -> frTargetDomain r == bdomain) federatedRequests - assertBool "Action is not a ConversationMemberUpdate" (isJust (getConvAction (sing @'ConversationMemberUpdateTag) (cuAction cUpdate))) - - -- conversation has been modified by action - updatedConv :: Conversation <- fmap responseJsonUnsafe $ getConvQualified alice cnv frTargetDomain r == cdomain) federatedRequests - - (_fr1, _cu1, _up1) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationMemberUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - (_fr2, convUpdate, receiptModeUpdate) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationReceiptModeUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - cruReceiptMode receiptModeUpdate @?= newReceiptMode - cuOrigUserId convUpdate @?= qbob - cuConvId convUpdate @?= qUnqualified cnv - cuAlreadyPresentUsers convUpdate @?= [qUnqualified qcharlie] - - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - wsAssertConvReceiptModeUpdate cnv qbob newReceiptMode n - where - _toOtherMember qid = OtherMember qid Nothing roleNameWireAdmin - _convView cnv usr = responseJsonUnsafeWithMsg "conversation" <$> getConv usr cnv - - parseConvUpdate :: FederatedRequest -> IO (FederatedRequest, ConversationUpdate) - parseConvUpdate rpc = do - frComponent rpc @?= Galley - frRPC rpc @?= "on-conversation-updated" - let convUpdate :: ConversationUpdate = fromRight (error $ "Could not parse ConversationUpdate from " <> show (frBody rpc)) $ A.eitherDecode (frBody rpc) - pure (rpc, convUpdate) - getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) getConvAction tquery (SomeConversationAction tag action) = case (tag, tquery) of diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index fcf5db5e90..66538f1309 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -23,34 +23,19 @@ where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) -import Data.Aeson (eitherDecode) -import Data.Domain -import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Misc import Data.Qualified -import Data.Singletons -import Federator.MockServer import Imports hiding (head) -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS -import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -63,8 +48,6 @@ tests s = ], test s "timer can be changed" messageTimerChange, test s "timer can be changed with the qualified endpoint" messageTimerChangeQualified, - test s "timer changes are propagated to remote users" messageTimerChangeWithRemotes, - test s "timer changes unavailable remotes" messageTimerUnavailableRemotes, test s "timer can't be set by conv member without allowed action" messageTimerChangeWithoutAllowedAction, test s "timer can't be set in 1:1 conversations" messageTimerChangeO2O, test s "setting the timer generates an event" messageTimerEvent @@ -143,86 +126,6 @@ messageTimerChangeQualified = do getConvQualified jane qcid !!! const timer1year === (cnvMessageTimer <=< responseJsonUnsafe) -messageTimerChangeWithRemotes :: TestM () -messageTimerChangeWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - -messageTimerUnavailableRemotes :: TestM () -messageTimerUnavailableRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - messageTimerChangeWithoutAllowedAction :: TestM () messageTimerChangeWithoutAllowedAction = do -- Create a team and a guest user diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 3e807e8eff..f74a264b71 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -20,20 +20,14 @@ module API.Roles where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) import Data.Aeson hiding (json) import Data.ByteString.Conversion (toByteString') -import Data.Domain import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Qualified import Data.Set qualified as Set -import Data.Singletons -import Federator.MockServer import Imports -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) @@ -42,13 +36,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -56,10 +44,6 @@ tests s = "Conversation roles" [ test s "conversation roles admin (and downgrade)" handleConversationRoleAdmin, test s "conversation roles member (and upgrade)" handleConversationRoleMember, - test s "conversation role update with remote users present" roleUpdateWithRemotes, - test s "conversation role update with remote users present remotes unavailable" roleUpdateWithRemotesUnavailable, - test s "conversation access update with remote users present" accessUpdateWithRemotes, - test s "conversation role update of remote member" roleUpdateRemoteMember, test s "get all conversation roles" testAllConversationRoles, test s "access role update with v2" testAccessRoleUpdateV2, test s "test access roles of new conversations" testConversationAccessRole @@ -161,236 +145,6 @@ handleConversationRoleMember = do wsAssertMemberUpdateWithRole qcid qalice bob roleNameWireAdmin wireAdminChecks cid bob alice chuck -roleUpdateRemoteMember :: TestM () -roleUpdateRemoteMember = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- Qualified <$> randomId <*> pure remoteDomain - let bob = qUnqualified qbob - - traverse_ (connectWithRemoteUser bob) [qalice, qcharlie] - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireMember)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireMember - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireMember))) - sort (F.cuAlreadyPresentUsers cu) @?= sort [qUnqualified qalice, qUnqualified qcharlie] - - liftIO . WS.assertMatch_ (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - - conv <- responseJsonError =<< getConvQualified bob qconv omQualifiedId m == qcharlie) (cmOthers (cnvMembers conv)) - liftIO $ - charlieAsMember - @=? Just - OtherMember - { omQualifiedId = qcharlie, - omService = Nothing, - omConvRoleName = roleNameWireMember - } - -roleUpdateWithRemotes :: TestM () -roleUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -roleUpdateWithRemotesUnavailable :: TestM () -roleUpdateWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -accessUpdateWithRemotes :: TestM () -accessUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - let access = ConversationAccessData (Set.singleton CodeAccess) (Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole, ServiceAccessRole]) - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedAccessUpdate bob qconv access - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu @?= SomeConversationAction (sing @'ConversationAccessDataTag) access - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvAccessUpdate - evtFrom e @?= qbob - evtData e @?= EdConvAccessUpdate access - -- | Given an admin, another admin and a member run all -- the necessary checks targeting the admin wireAdminChecks ::