Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3-bug-fixes/remote-member-removal-notification
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions integration/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
, cql-io
, cryptonite
, data-default
, data-timeout
, directory
, errors
, exceptions
Expand Down Expand Up @@ -86,6 +87,7 @@ mkDerivation {
cql-io
cryptonite
data-default
data-timeout
directory
errors
exceptions
Expand Down
4 changes: 4 additions & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ library
Notifications
RunAllTests
SetupHelpers
Test.AccessUpdate
Test.AssetDownload
Test.B2B
Test.Brig
Expand All @@ -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
Expand Down Expand Up @@ -146,6 +149,7 @@ library
, cql-io
, cryptonite
, data-default
, data-timeout
, directory
, errors
, exceptions
Expand Down
172 changes: 167 additions & 5 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -210,16 +226,162 @@ 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
(convDomain, convId) <- objQid qcnv
(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)
59 changes: 57 additions & 2 deletions integration/test/Notifications.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
)
45 changes: 37 additions & 8 deletions integration/test/SetupHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading