From 22669d0423bc792e650150b01568870699ff1d25 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Mon, 13 Jun 2022 11:57:51 +0100 Subject: [PATCH] Add default contact request If a contact update or a legacy contact request is sent, we create a notification for the user in the activity center so it can be replied to. If at a later date a new contact request is received from the same user, this will replace it, so the proper message can be displayed. --- VERSION | 2 +- protocol/activity_center_persistence.go | 79 ++++++-- protocol/messenger_contact_requests_test.go | 204 +++++++++++++++++++- protocol/messenger_contacts.go | 6 + protocol/messenger_handler.go | 79 +++++++- protocol/messenger_response.go | 13 ++ protocol/push_notification_test.go | 186 ++++++++++++++++++ 7 files changed, 538 insertions(+), 31 deletions(-) diff --git a/VERSION b/VERSION index 7bb21aff8..bd720cd00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.102.0 +0.102.2 diff --git a/protocol/activity_center_persistence.go b/protocol/activity_center_persistence.go index f2107910b..396eb52ad 100644 --- a/protocol/activity_center_persistence.go +++ b/protocol/activity_center_persistence.go @@ -416,26 +416,30 @@ func (db sqlitePersistence) GetActivityCenterNotificationsByID(ids []types.HexBy func (db sqlitePersistence) GetActivityCenterNotificationByID(id types.HexBytes) (*ActivityCenterNotification, error) { row := db.db.QueryRow(` - SELECT - a.id, - a.timestamp, - a.notification_type, - a.chat_id, - a.read, - a.accepted, - a.dismissed, - a.message, - c.last_message, - a.reply_message, - c.name, - a.author - FROM activity_center_notifications a - LEFT JOIN chats c - ON - c.id = a.chat_id - WHERE a.id = ?`, id) + SELECT + a.id, + a.timestamp, + a.notification_type, + a.chat_id, + a.read, + a.accepted, + a.dismissed, + a.message, + c.last_message, + a.reply_message, + c.name, + a.author + FROM activity_center_notifications a + LEFT JOIN chats c + ON + c.id = a.chat_id + WHERE a.id = ?`, id) - return db.unmarshalActivityCenterNotificationRow(row) + notification, err := db.unmarshalActivityCenterNotificationRow(row) + if err == sql.ErrNoRows { + return nil, nil + } + return notification, err } func (db sqlitePersistence) ActivityCenterNotifications(currCursor string, limit uint64) (string, []*ActivityCenterNotification, error) { @@ -673,3 +677,40 @@ func (db sqlitePersistence) UnreadActivityCenterNotificationsCount() (uint64, er err := db.db.QueryRow(`SELECT COUNT(1) FROM activity_center_notifications WHERE NOT read AND NOT dismissed AND NOT accepted`).Scan(&count) return count, err } + +func (db sqlitePersistence) ActiveContactRequestNotification(contactID string) (*ActivityCenterNotification, error) { + row := db.db.QueryRow(` + SELECT + a.id, + a.timestamp, + a.notification_type, + a.chat_id, + a.read, + a.accepted, + a.dismissed, + a.message, + c.last_message, + a.reply_message, + c.name, + a.author + FROM activity_center_notifications a + LEFT JOIN chats c + ON + c.id = a.chat_id + WHERE NOT dismissed AND NOT a.accepted AND notification_type = ? AND author = ?`, ActivityCenterNotificationTypeContactRequest, contactID) + notification, err := db.unmarshalActivityCenterNotificationRow(row) + if err == sql.ErrNoRows { + return nil, nil + } + return notification, err +} + +func (db sqlitePersistence) RemoveAllContactRequestActivityCenterNotifications(chatID string) error { + _, err := db.db.Exec(` + DELETE FROM activity_center_notifications + WHERE + chat_id = ? + AND notification_type = ? + `, chatID, ActivityCenterNotificationTypeContactRequest) + return err +} diff --git a/protocol/messenger_contact_requests_test.go b/protocol/messenger_contact_requests_test.go index 9aeb030d4..63224dcfc 100644 --- a/protocol/messenger_contact_requests_test.go +++ b/protocol/messenger_contact_requests_test.go @@ -330,8 +330,9 @@ func (s *MessengerContactRequestSuite) TestReceiveAcceptAndRetractContactRequest s.Require().Len(contactRequests, 1) s.Require().Equal(contactRequests[0].ContactRequestState, common.ContactRequestStatePending) + cid := resp.ActivityCenterNotifications()[0].Message.ID // Accept contact request, receiver side - resp, err = theirMessenger.AcceptContactRequest(context.Background(), &requests.AcceptContactRequest{ID: types.Hex2Bytes(contactRequests[0].ID)}) + resp, err = theirMessenger.AcceptContactRequest(context.Background(), &requests.AcceptContactRequest{ID: types.Hex2Bytes(cid)}) s.Require().NoError(err) // Make sure the message is updated @@ -802,7 +803,6 @@ func (s *MessengerContactRequestSuite) TestDismissLatestContactRequestForContact } -/* Disabling as currently there's an issue with duplicated contact requests func (s *MessengerContactRequestSuite) TestReceiveAndAcceptLegacyContactRequest() { theirMessenger := s.newMessenger(s.shh) @@ -873,4 +873,202 @@ func (s *MessengerContactRequestSuite) TestReceiveAndAcceptLegacyContactRequest( // Make sure we consider them a mutual contact, receiver side mutualContacts := theirMessenger.MutualContacts() s.Require().Len(mutualContacts, 1) -} */ +} + +func (s *MessengerContactRequestSuite) TestLegacyContactRequestNotifications() { + + theirMessenger := s.newMessenger(s.shh) + _, err := theirMessenger.Start() + s.Require().NoError(err) + + contactID := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) + request := &requests.AddContact{ + ID: types.Hex2Bytes(contactID), + } + + // Send legacy contact request + resp, err := s.m.AddContact(context.Background(), request) + s.Require().NoError(err) + + s.Require().NotNil(resp) + + // Make sure contact is added on the sender side + contacts := s.m.AddedContacts() + s.Require().Len(contacts, 1) + s.Require().Equal(ContactRequestStateSent, contacts[0].ContactRequestState) + + // Wait for the message to reach its destination + resp, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { + return len(r.Contacts) > 0 && len(r.ActivityCenterNotifications()) == 1 + }, + "no messages", + ) + + s.Require().NoError(err) + + notification := resp.ActivityCenterNotifications()[0] + + // Check contact request has been received + s.Require().NoError(err) + + // Check activity center notification is of the right type + s.Require().Equal(ActivityCenterNotificationTypeContactRequest, notification.Type) + s.Require().NotNil(notification.Type) + s.Require().Equal(common.ContactRequestStatePending, notification.Message.ContactRequestState) + + // Check the contact state is correctly set + s.Require().Len(resp.Contacts, 1) + s.Require().Equal(ContactRequestStateReceived, resp.Contacts[0].ContactRequestState) + + // Send new contact request + resp, err = s.m.AddContact(context.Background(), request) + s.Require().NoError(err) + + s.Require().NotNil(resp) + + crRequest := &requests.SendContactRequest{ + ID: types.Hex2Bytes(contactID), + Message: "hello", + } + + myID := types.EncodeHex(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + + // Send contact request + resp, err = s.m.SendContactRequest(context.Background(), crRequest) + s.Require().NoError(err) + + paginationResponse, err := theirMessenger.ActivityCenterNotifications("", 10) + + s.Require().NoError(err) + s.Require().Len(paginationResponse.Notifications, 1) + + // Wait for the message to reach its destination + resp, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { + return len(r.ActivityCenterNotifications()) == 2 + }, + "no messages", + ) + s.Require().NoError(err) + + activityCenterNotifications := resp.ActivityCenterNotifications() + var newNotification, oldNotification *ActivityCenterNotification + if activityCenterNotifications[0].Message.ID == defaultContactRequestID(myID) { + oldNotification = activityCenterNotifications[0] + newNotification = activityCenterNotifications[1] + } else { + newNotification = activityCenterNotifications[0] + oldNotification = activityCenterNotifications[1] + } + + s.Require().True(oldNotification.Dismissed) + s.Require().False(newNotification.Dismissed) + + paginationResponse, err = theirMessenger.ActivityCenterNotifications("", 10) + + s.Require().NoError(err) + s.Require().Len(paginationResponse.Notifications, 1) +} + +func (s *MessengerContactRequestSuite) TestReceiveMultipleLegacy() { + + theirMessenger := s.newMessenger(s.shh) + _, err := theirMessenger.Start() + s.Require().NoError(err) + + s.Require().NoError(theirMessenger.settings.SaveSettingField(settings.MutualContactEnabled, true)) + s.Require().NoError(s.m.settings.SaveSettingField(settings.MutualContactEnabled, true)) + + contactID := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) + request := &requests.AddContact{ + ID: types.Hex2Bytes(contactID), + } + + // Send legacy contact request + resp, err := s.m.AddContact(context.Background(), request) + s.Require().NoError(err) + + s.Require().NotNil(resp) + + // Make sure contact is added on the sender side + contacts := s.m.AddedContacts() + s.Require().Len(contacts, 1) + s.Require().Equal(ContactRequestStateSent, contacts[0].ContactRequestState) + + // Wait for the message to reach its destination + resp, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { + return len(r.Contacts) > 0 && len(r.ActivityCenterNotifications()) == 1 + }, + "no messages", + ) + + s.Require().NoError(err) + + notification := resp.ActivityCenterNotifications()[0] + + // Check contact request has been received + s.Require().NoError(err) + + // Check activity center notification is of the right type + s.Require().Equal(ActivityCenterNotificationTypeContactRequest, notification.Type) + s.Require().NotNil(notification.Type) + s.Require().Equal(common.ContactRequestStatePending, notification.Message.ContactRequestState) + + // Check the contact state is correctly set + s.Require().Len(resp.Contacts, 1) + s.Require().Equal(ContactRequestStateReceived, resp.Contacts[0].ContactRequestState) + + // Remove contact + + _, err = s.m.RetractContactRequest(&requests.RetractContactRequest{ContactID: types.Hex2Bytes(contactID)}) + s.Require().NoError(err) + + // Wait for the message to reach its destination + resp, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { + return len(r.Contacts) == 1 + }, + "no messages", + ) + s.Require().NoError(err) + + // Make sure it's not a contact anymore + s.Require().Equal(ContactRequestStateNone, resp.Contacts[0].ContactRequestState) + + // Re-add user + resp, err = s.m.AddContact(context.Background(), request) + s.Require().NoError(err) + s.Require().NotNil(resp) + + // Wait for the message to reach its destination + resp, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { + return len(r.Contacts) > 0 && len(r.ActivityCenterNotifications()) == 1 + }, + "no messages", + ) + + s.Require().NoError(err) + + notification = resp.ActivityCenterNotifications()[0] + + // Check contact request has been received + s.Require().NoError(err) + + // Check activity center notification is of the right type + s.Require().Equal(ActivityCenterNotificationTypeContactRequest, notification.Type) + s.Require().NotNil(notification.Type) + s.Require().Equal(common.ContactRequestStatePending, notification.Message.ContactRequestState) + + // Check the contact state is correctly set + s.Require().Len(resp.Contacts, 1) + s.Require().Equal(ContactRequestStateReceived, resp.Contacts[0].ContactRequestState) + +} diff --git a/protocol/messenger_contacts.go b/protocol/messenger_contacts.go index 67e914b94..c3e8bfe6b 100644 --- a/protocol/messenger_contacts.go +++ b/protocol/messenger_contacts.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/golang/protobuf/proto" + "go.uber.org/zap" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/common" @@ -22,6 +23,7 @@ func (m *Messenger) AcceptContactRequest(ctx context.Context, request *requests. contactRequest, err := m.persistence.MessageByID(request.ID.String()) if err != nil { + m.logger.Error("could not find contact request message", zap.Error(err)) return nil, err } @@ -848,3 +850,7 @@ func (m *Messenger) DismissLatestContactRequestForContact(ctx context.Context, r func (m *Messenger) PendingContactRequests(cursor string, limit int) ([]*common.Message, string, error) { return m.persistence.PendingContactRequests(cursor, limit) } + +func defaultContactRequestID(contactID string) string { + return "0x" + types.Bytes2Hex(append(types.Hex2Bytes(contactID), 0x20)) +} diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index caa8df301..ee1c845db 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -231,14 +231,68 @@ func (m *Messenger) createMessageNotification(chat *Chat, messageState *Received } } +func (m *Messenger) PendingNotificationContactRequest(contactID string) (*ActivityCenterNotification, error) { + return m.persistence.ActiveContactRequestNotification(contactID) +} + func (m *Messenger) createContactRequestNotification(contact *Contact, messageState *ReceivedMessageState, contactRequest *common.Message) error { - // Legacy contact request - if contactRequest == nil { + if contactRequest == nil || contactRequest.ContactRequestState == common.ContactRequestStatePending { + notification, err := m.PendingNotificationContactRequest(contact.ID) + if err != nil { + return err + } + // If there's already a notification, we will check whether is a default notification + // that has not been dismissed (nor accepted???) + // If it is, we replace it with a non-default, since it contains a message + if notification != nil { + // Check if it's the default notification + if notification.Message.ID == defaultContactRequestID(contact.ID) { + // Nothing to do, we already have a default notification + if contactRequest == nil { + return nil + } + // We first dismiss it in the database + err := m.persistence.DismissActivityCenterNotifications([]types.HexBytes{types.Hex2Bytes(notification.Message.ID)}) + if err != nil { + return err + } + // we mark the notification as dismissed + notification.Dismissed = true + // We remove it from the response, since the client has never seen it, better to just remove it + found := messageState.Response.RemoveActivityCenterNotification(notification.Message.ID) + // Otherwise, it means we have already passed it to the client, so we add it with a `dismissed` flag + // so it can clean up + if !found { + messageState.Response.AddActivityCenterNotification(notification) + } + } + } + } + + // Legacy//ContactUpdate contact request + if contactRequest == nil { if messageState.CurrentMessageState == nil || messageState.CurrentMessageState.MessageID == "" { return errors.New("no available id") } + // We use a known id so that we can check if already in the database + defaultID := defaultContactRequestID(contact.ID) + + // Pull one from the db if there + notification, err := m.persistence.GetActivityCenterNotificationByID(types.FromHex(defaultID)) + if err != nil { + return err + } + + // if the notification is accepted, we clear it, as this one will replace it + if notification != nil && notification.Accepted { + err = m.persistence.DeleteActivityCenterNotification(types.FromHex(defaultID)) + if err != nil { + return err + } + } + contactRequest = &common.Message{} contactRequest.WhisperTimestamp = messageState.CurrentMessageState.WhisperTimestamp @@ -247,9 +301,9 @@ func (m *Messenger) createContactRequestNotification(contact *Contact, messageSt contactRequest.From = contact.ID contactRequest.ContentType = protobuf.ChatMessage_CONTACT_REQUEST contactRequest.Clock = messageState.CurrentMessageState.Message.Clock - contactRequest.ID = messageState.CurrentMessageState.MessageID + contactRequest.ID = defaultID contactRequest.ContactRequestState = common.ContactRequestStatePending - err := contactRequest.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey)) + err = contactRequest.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey)) if err != nil { return err } @@ -260,7 +314,6 @@ func (m *Messenger) createContactRequestNotification(contact *Contact, messageSt if err != nil { return err } - } notification := &ActivityCenterNotification{ @@ -669,7 +722,6 @@ func (m *Messenger) HandleAcceptContactRequest(state *ReceivedMessageState, mess state.Response.AddMessage(request) return nil - } func (m *Messenger) HandleRetractContactRequest(state *ReceivedMessageState, message protobuf.RetractContactRequest) error { @@ -687,7 +739,14 @@ func (m *Messenger) HandleRetractContactRequest(state *ReceivedMessageState, mes // We remove from our old contacts only if mutual contacts are enabled if mutualContactEnabled { contact.Added = false + } + // We remove anything that's related to this contact request + err = m.persistence.RemoveAllContactRequestActivityCenterNotifications(contact.ID) + if err != nil { + return err + } + contact.HasAddedUs = false contact.ContactRequestClock = message.Clock contact.ContactRequestRetracted() @@ -738,7 +797,7 @@ func (m *Messenger) HandleContactUpdate(state *ReceivedMessageState, message pro contact.LastUpdated = message.Clock state.ModifiedContacts.Store(contact.ID, true) state.AllContacts.Store(contact.ID, contact) - /* Disabling for now in order to avoid duplicated contact requests in activity center + // Has the user added us? if contact.ContactRequestState == ContactRequestStateNone { contact.ContactRequestState = ContactRequestStateReceived err = m.createContactRequestNotification(contact, state, nil) @@ -746,7 +805,11 @@ func (m *Messenger) HandleContactUpdate(state *ReceivedMessageState, message pro m.logger.Warn("could not create contact request notification", zap.Error(err)) } - }*/ + // Has the user replied to a default contact request + } else if contact.ContactRequestState == ContactRequestStateSent { + contact.ContactRequestState = ContactRequestStateMutual + + } } if chat.LastClockValue < message.Clock { diff --git a/protocol/messenger_response.go b/protocol/messenger_response.go index 9cd985edc..f88cd9d20 100644 --- a/protocol/messenger_response.go +++ b/protocol/messenger_response.go @@ -360,6 +360,19 @@ func (r *MessengerResponse) AddActivityCenterNotification(n *ActivityCenterNotif r.activityCenterNotifications[n.ID.String()] = n } +func (r *MessengerResponse) RemoveActivityCenterNotification(id string) bool { + if r.activityCenterNotifications == nil { + return false + } + + if _, ok := r.activityCenterNotifications[id]; ok { + delete(r.activityCenterNotifications, id) + return true + } + + return false +} + func (r *MessengerResponse) ActivityCenterNotifications() []*ActivityCenterNotification { var ns []*ActivityCenterNotification for _, n := range r.activityCenterNotifications { diff --git a/protocol/push_notification_test.go b/protocol/push_notification_test.go index 10b8e29a0..f473aafef 100644 --- a/protocol/push_notification_test.go +++ b/protocol/push_notification_test.go @@ -977,3 +977,189 @@ func (s *MessengerPushNotificationSuite) TestReceivePushNotificationCommunityReq s.Require().NoError(alice.Shutdown()) s.Require().NoError(server.Shutdown()) } + +func (s *MessengerPushNotificationSuite) TestReceivePushNotificationPairedDevices() { + + bob1 := s.m + bob2, err := newMessengerWithKey(s.shh, s.m.identity, s.logger, []Option{WithPushNotifications()}) + s.Require().NoError(err) + + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + server := s.newPushNotificationServer(s.shh, serverKey) + + alice := s.newMessenger(s.shh) + // start alice and enable sending push notifications + _, err = alice.Start() + s.Require().NoError(err) + s.Require().NoError(alice.EnableSendingPushNotifications()) + bobInstallationIDs := []string{bob1.installationID, bob2.installationID} + + // Register bob1 + err = bob1.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey, pushnotificationclient.ServerTypeCustom) + s.Require().NoError(err) + + err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob1.RetrieveAll() + if err != nil { + return err + } + registered, err := bob1.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + bobServers, err := bob1.GetPushNotificationsServers() + if err != nil { + return err + } + + if len(bobServers) == 0 { + return errors.New("not registered") + } + + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob1Servers, err := bob1.GetPushNotificationsServers() + s.Require().NoError(err) + + // Register bob2 + err = bob2.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey, pushnotificationclient.ServerTypeCustom) + s.Require().NoError(err) + + err = bob2.RegisterForPushNotifications(context.Background(), bob2DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN) + s.Require().NoError(err) + + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob2.RetrieveAll() + if err != nil { + return err + } + + registered, err := bob2.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + bobServers, err := bob2.GetPushNotificationsServers() + if err != nil { + return err + } + + if len(bobServers) == 0 { + return errors.New("not registered") + } + + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob2Servers, err := bob2.GetPushNotificationsServers() + s.Require().NoError(err) + + // Create one to one chat & send message + pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.transport) + s.Require().NoError(alice.SaveChat(chat)) + inputMessage := buildTestMessage(*chat) + response, err := alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + messageIDString := response.Messages()[0].ID + messageID, err := hex.DecodeString(messageIDString[2:]) + s.Require().NoError(err) + + infoMap := make(map[string]*pushnotificationclient.PushNotificationInfo) + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + for _, i := range info { + infoMap[i.AccessToken] = i + } + + // Check we have replies for both bob1 and bob2 + if len(infoMap) != 2 { + return errors.New("info not fetched") + } + return nil + + }) + + s.Require().Len(infoMap, 2) + + // Check we have replies for both bob1 and bob2 + var bob1Info, bob2Info *pushnotificationclient.PushNotificationInfo + + bob1Info = infoMap[bob1Servers[0].AccessToken] + bob2Info = infoMap[bob2Servers[0].AccessToken] + + s.Require().NotNil(bob1Info) + s.Require().Equal(bob1.installationID, bob1Info.InstallationID) + s.Require().Equal(bob1Servers[0].AccessToken, bob1Info.AccessToken) + s.Require().Equal(&bob1.identity.PublicKey, bob1Info.PublicKey) + + s.Require().NotNil(bob2Info) + s.Require().Equal(bob2.installationID, bob2Info.InstallationID) + s.Require().Equal(bob2Servers[0].AccessToken, bob2Info.AccessToken) + s.Require().Equal(&bob2.identity.PublicKey, bob2Info.PublicKey) + + retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + + s.Require().NoError(err) + s.Require().NotNil(retrievedNotificationInfo) + s.Require().Len(retrievedNotificationInfo, 2) + + var sentNotification *pushnotificationclient.SentNotification + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob1.identity.PublicKey), bob1.installationID, messageID) + if err != nil { + return err + } + if sentNotification == nil { + return errors.New("sent notification not found") + } + if !sentNotification.Success { + return errors.New("sent notification not successul") + } + return nil + }) + s.Require().NoError(err) + s.Require().NoError(bob2.Shutdown()) + s.Require().NoError(alice.Shutdown()) + s.Require().NoError(server.Shutdown()) +}